| 1 | "use client"; |
| 2 | |
| 3 | import { |
| 4 | ArrowDown, |
| 5 | ArrowRight, |
| 6 | Check, |
| 7 | Globe, |
| 8 | ImageIcon, |
| 9 | PenTool, |
| 10 | Plus, |
| 11 | Sparkles, |
| 12 | type LucideIcon, |
| 13 | } from "lucide-react"; |
| 14 | import { useState } from "react"; |
| 15 | |
| 16 | import { PRESENTATION_PORTAL_CONTENT_CLASS } from "@/components/presentation/overlay-layers"; |
| 17 | import { Button } from "@/components/ui/button"; |
| 18 | import { |
| 19 | DropdownMenu, |
| 20 | DropdownMenuContent, |
| 21 | DropdownMenuSeparator, |
| 22 | DropdownMenuTrigger, |
| 23 | } from "@/components/ui/dropdown-menu"; |
| 24 | import { cn } from "@/lib/utils"; |
| 25 | import { usePresentationState } from "@/states/presentation-state"; |
| 26 | import { useSlideEditorContext } from "./SlideEditorContext"; |
| 27 | |
| 28 | const makeVisualAction: MagicAction = { |
| 29 | icon: ImageIcon, |
| 30 | label: "Make visual", |
| 31 | prompt: |
| 32 | "Redesign this card to be more visual and presentation-ready. Convert dense text into a stronger visual hierarchy while preserving the meaning.", |
| 33 | }; |
| 34 | |
| 35 | interface MagicAction { |
| 36 | icon: LucideIcon; |
| 37 | label: string; |
| 38 | prompt: string; |
| 39 | } |
| 40 | |
| 41 | export function MagicMenuDropdown() { |
| 42 | const [isOpen, setIsOpen] = useState(false); |
| 43 | const language = usePresentationState((s) => s.language); |
| 44 | const { |
| 45 | aiInput, |
| 46 | setAiInput, |
| 47 | handleAiSubmit, |
| 48 | handleAiKeyDown, |
| 49 | handleImageEdit, |
| 50 | handleMagicPrompt, |
| 51 | } = useSlideEditorContext(); |
| 52 | |
| 53 | const runMagicAction = (prompt: string) => { |
| 54 | handleMagicPrompt(prompt); |
| 55 | setIsOpen(false); |
| 56 | }; |
| 57 | |
| 58 | const writingActions: MagicAction[] = [ |
| 59 | { |
| 60 | icon: PenTool, |
| 61 | label: "Improve writing", |
| 62 | prompt: |
| 63 | "Improve the writing on this card. Make it sharper, clearer, and more executive-ready while preserving the original meaning.", |
| 64 | }, |
| 65 | { |
| 66 | icon: Check, |
| 67 | label: "Fix spelling", |
| 68 | prompt: |
| 69 | "Fix spelling, grammar, punctuation, and awkward phrasing on this card without changing the meaning or layout.", |
| 70 | }, |
| 71 | { |
| 72 | icon: Globe, |
| 73 | label: "Translate", |
| 74 | prompt: `Translate this card into the presentation language (${language}). Preserve the slide structure and meaning.`, |
| 75 | }, |
| 76 | { |
| 77 | icon: ArrowDown, |
| 78 | label: "Simplify", |
| 79 | prompt: |
| 80 | "Simplify this card. Use shorter sentences and clearer wording while keeping the same core points.", |
| 81 | }, |
| 82 | ]; |
| 83 | |
| 84 | return ( |
| 85 | <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> |
| 86 | <DropdownMenuTrigger asChild> |
| 87 | <Button |
| 88 | variant="ghost" |
| 89 | size="icon" |
| 90 | className="size-8! gap-1 rounded-full border border-white/20 bg-background/50 shadow backdrop-blur-md transition-all hover:border-primary/40 hover:bg-background/90 hover:text-primary" |
| 91 | > |
| 92 | <Sparkles className="size-4 text-muted-foreground" /> |
| 93 | </Button> |
| 94 | </DropdownMenuTrigger> |
| 95 | <DropdownMenuContent |
| 96 | align="start" |
| 97 | className={cn( |
| 98 | PRESENTATION_PORTAL_CONTENT_CLASS, |
| 99 | "w-86 overflow-hidden rounded-xl border-border/70 bg-neutral-950 p-0 text-neutral-50 shadow-2xl", |
| 100 | )} |
| 101 | > |
| 102 | <div className="border-b border-white/10 bg-linear-to-b from-white/10 to-transparent p-4"> |
| 103 | <div className="mb-3 flex items-center gap-2"> |
| 104 | <div className="flex size-7 items-center justify-center rounded-full bg-primary text-primary-foreground"> |
| 105 | <Sparkles className="size-3.5" /> |
| 106 | </div> |
| 107 | <h3 className="text-sm font-semibold">Edit this card</h3> |
| 108 | </div> |
| 109 | |
| 110 | <div className="flex items-center gap-2 rounded-lg border border-white/15 bg-white/6 p-2 shadow-inner transition-colors focus-within:border-primary/70"> |
| 111 | <input |
| 112 | aria-label="magic menu dropdown control" |
| 113 | type="text" |
| 114 | value={aiInput} |
| 115 | onChange={(e) => setAiInput(e.target.value)} |
| 116 | onKeyDown={handleAiKeyDown} |
| 117 | placeholder="How would you like to edit this card?" |
| 118 | className="min-w-0 flex-1 bg-transparent px-1 text-sm text-white outline-none placeholder:text-neutral-500" |
| 119 | /> |
| 120 | <button |
| 121 | type="button" |
| 122 | onClick={() => { |
| 123 | handleAiSubmit(); |
| 124 | if (aiInput.trim()) { |
| 125 | setIsOpen(false); |
| 126 | } |
| 127 | }} |
| 128 | disabled={!aiInput.trim()} |
| 129 | className="flex size-8 shrink-0 items-center justify-center rounded-full bg-white text-neutral-950 transition hover:bg-primary hover:text-primary-foreground disabled:cursor-not-allowed disabled:bg-white/10 disabled:text-neutral-600" |
| 130 | > |
| 131 | <ArrowRight className="size-4" /> |
| 132 | </button> |
| 133 | </div> |
| 134 | </div> |
| 135 | |
| 136 | <div className="p-3"> |
| 137 | <button |
| 138 | type="button" |
| 139 | onClick={() => |
| 140 | runMagicAction( |
| 141 | "Try a new layout for this card. Keep the same message, but improve the visual balance, hierarchy, and slide structure.", |
| 142 | ) |
| 143 | } |
| 144 | className="mb-3 flex w-full items-center justify-between rounded-lg border border-primary/25 bg-primary/10 px-3 py-2.5 text-left transition hover:border-primary/50 hover:bg-primary/20" |
| 145 | > |
| 146 | <span className="flex items-center gap-2 text-sm font-semibold"> |
| 147 | <Sparkles className="size-4 text-primary" /> |
| 148 | Try new layout |
| 149 | </span> |
| 150 | <ArrowRight className="size-4 text-neutral-500" /> |
| 151 | </button> |
| 152 | |
| 153 | <DropdownMenuSeparator className="bg-white/10" /> |
| 154 | |
| 155 | <div className="mt-3 mb-4"> |
| 156 | <h4 className="mb-2 px-1 text-[0.68rem] font-semibold text-neutral-500 uppercase"> |
| 157 | Writing |
| 158 | </h4> |
| 159 | <div className="mb-2 grid grid-cols-2 gap-2"> |
| 160 | {writingActions.map((action) => ( |
| 161 | <MagicActionButton |
| 162 | key={action.label} |
| 163 | action={action} |
| 164 | onClick={() => runMagicAction(action.prompt)} |
| 165 | /> |
| 166 | ))} |
| 167 | </div> |
| 168 | </div> |
| 169 | |
| 170 | <div> |
| 171 | <h4 className="mb-2 px-1 text-[0.68rem] font-semibold text-neutral-500 uppercase"> |
| 172 | Image |
| 173 | </h4> |
| 174 | <div className="grid grid-cols-2 gap-2"> |
| 175 | <MagicActionButton |
| 176 | action={makeVisualAction} |
| 177 | onClick={() => runMagicAction(makeVisualAction.prompt)} |
| 178 | /> |
| 179 | <button |
| 180 | type="button" |
| 181 | className={cn( |
| 182 | "flex min-h-11 items-center gap-2 rounded-lg border border-white/10 bg-white/[0.07] px-3 py-2 text-left text-sm font-medium text-neutral-100 transition", |
| 183 | "hover:border-primary/50 hover:bg-white/12 hover:text-white focus-visible:ring-2 focus-visible:ring-primary/70 focus-visible:outline-none", |
| 184 | )} |
| 185 | onClick={() => { |
| 186 | handleImageEdit(); |
| 187 | setIsOpen(false); |
| 188 | }} |
| 189 | > |
| 190 | <Plus className="size-4" /> |
| 191 | Add image |
| 192 | </button> |
| 193 | </div> |
| 194 | </div> |
| 195 | </div> |
| 196 | </DropdownMenuContent> |
| 197 | </DropdownMenu> |
| 198 | ); |
| 199 | } |
| 200 | |
| 201 | function MagicActionButton({ |
| 202 | action, |
| 203 | onClick, |
| 204 | }: { |
| 205 | action: MagicAction; |
| 206 | onClick: () => void; |
| 207 | }) { |
| 208 | const Icon = action.icon; |
| 209 | |
| 210 | return ( |
| 211 | <button |
| 212 | type="button" |
| 213 | onClick={onClick} |
| 214 | className={cn( |
| 215 | "flex min-h-11 items-center gap-2 rounded-lg border border-white/10 bg-white/[0.07] px-3 py-2 text-left text-sm font-medium text-neutral-100 transition", |
| 216 | "hover:border-primary/50 hover:bg-white/12 hover:text-white focus-visible:ring-2 focus-visible:ring-primary/70 focus-visible:outline-none", |
| 217 | )} |
| 218 | > |
| 219 | <Icon className="size-4 shrink-0 text-neutral-400" /> |
| 220 | <span className="min-w-0 leading-tight">{action.label}</span> |
| 221 | </button> |
| 222 | ); |
| 223 | } |
| 224 |