返回 presentation-ai
GenerateImageSlidesButton.tsx
根目录 / src / components / notebook / presentation / components / outline / GenerateImageSlidesButton.tsx
1 import { type ImageModelList } from "@/constants/image-models";
2 import { useState } from "react";
3
4 const GoogleLogo = () => (
5 <svg width="18" height="18" viewBox="0 0 24 24">
6 <path
7 d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
8 fill="#4285F4"
9 />
10 <path
11 d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
12 fill="#34A853"
13 />
14 <path
15 d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
16 fill="#FBBC05"
17 />
18 <path
19 d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
20 fill="#EA4335"
21 />
22 </svg>
23 );
24
25 const FluxLogo = () => (
26 <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
27 <rect width="24" height="24" rx="5" fill="#0a0a0a" />
28 <path d="M6 7h5l-3 5h3l-5 5 2-3.5H6L8.5 7" fill="url(#fg)" />
29 <path
30 d="M13 7h5l-3 5h3l-5 5 2-3.5H13L15.5 7"
31 fill="url(#fg)"
32 opacity="0.5"
33 />
34 <defs>
35 <linearGradient id="fg" x1="0%" y1="0%" x2="100%" y2="100%">
36 <stop offset="0%" stopColor="#c084fc" />
37 <stop offset="100%" stopColor="#818cf8" />
38 </linearGradient>
39 </defs>
40 </svg>
41 );
42
43 const MODELS = [
44 {
45 id: "fal-ai/nano-banana-pro",
46 name: "Nano Banana Pro",
47 provider: "Google",
48 logo: <GoogleLogo />,
49 premium: true,
50 },
51 {
52 id: "fal-ai/flux-2/flash",
53 name: "Flux 2.0",
54 provider: "Black Forest Labs",
55 logo: <FluxLogo />,
56 },
57 ];
58
59 import { Button } from "@/components/ui/button";
60 import { ButtonGroup } from "@/components/ui/button-group";
61 import {
62 DropdownMenu,
63 DropdownMenuContent,
64 DropdownMenuItem,
65 DropdownMenuLabel,
66 DropdownMenuTrigger,
67 } from "@/components/ui/dropdown-menu";
68
69 export function GenerateImageSlidesButton({
70 isGenerating,
71 disabled = false,
72 onGenerateImageSlides,
73 }: {
74 isGenerating: boolean;
75 disabled?: boolean;
76 onGenerateImageSlides: (model: ImageModelList) => void;
77 }) {
78 const [sel, setSel] = useState<(typeof MODELS)[0]>(
79 MODELS[0] as (typeof MODELS)[0],
80 );
81
82 const handleGenerateClick = () => {
83 if (!sel) return;
84 onGenerateImageSlides(sel.id as ImageModelList);
85 };
86
87 return (
88 <ButtonGroup className="w-full sm:w-fit">
89 <DropdownMenu>
90 <DropdownMenuTrigger asChild>
91 <Button
92 variant="outline"
93 title="Change model"
94 size="lg"
95 className="flex shrink-0 items-center justify-center bg-background px-3 text-muted-foreground hover:bg-accent focus-visible:ring-0 focus-visible:ring-offset-0 sm:h-10 sm:px-2.5"
96 >
97 {sel?.logo}
98 <svg
99 width="10"
100 height="10"
101 viewBox="0 0 24 24"
102 fill="none"
103 stroke="currentColor"
104 strokeWidth="3"
105 strokeLinecap="round"
106 strokeLinejoin="round"
107 className="ml-1 opacity-50 transition-transform group-data-[state=open]:rotate-180"
108 >
109 <polyline points="6 9 12 15 18 9" />
110 </svg>
111 </Button>
112 </DropdownMenuTrigger>
113
114 <DropdownMenuContent align="start" className="w-[220px] rounded-xl p-1">
115 <DropdownMenuLabel className="px-2 pt-1 pb-2 text-[11px] font-semibold tracking-wide text-muted-foreground uppercase">
116 Model
117 </DropdownMenuLabel>
118 {MODELS.map((m) => (
119 <DropdownMenuItem
120 key={m.id}
121 onClick={() => setSel(m)}
122 className={`flex w-full cursor-pointer items-center justify-between rounded-lg px-2.5 py-2 ${
123 sel?.id === m.id ? "bg-primary/10" : ""
124 }`}
125 >
126 <div>
127 <div className="flex items-center gap-1.5">
128 <span className="text-[13px] font-semibold text-foreground">
129 {m.name}
130 </span>
131 {m.premium && (
132 <span className="rounded-full border border-amber-300 bg-linear-to-br from-amber-100 to-amber-200 px-1.5 py-px text-[9px] font-bold tracking-wide text-amber-700 uppercase">
133 Premium
134 </span>
135 )}
136 </div>
137 <div className="text-[11px] text-muted-foreground">
138 {m.provider}
139 </div>
140 </div>
141 {sel?.id === m.id && (
142 <svg
143 width="14"
144 height="14"
145 viewBox="0 0 24 24"
146 fill="none"
147 stroke="currentColor"
148 strokeWidth="3"
149 strokeLinecap="round"
150 strokeLinejoin="round"
151 className="stroke-[3px] text-primary"
152 >
153 <polyline points="20 6 9 17 4 12" />
154 </svg>
155 )}
156 </DropdownMenuItem>
157 ))}
158 </DropdownMenuContent>
159 </DropdownMenu>
160
161 <Button
162 variant="outline"
163 size="lg"
164 onClick={handleGenerateClick}
165 disabled={isGenerating || disabled}
166 className={`flex flex-1 items-center justify-center gap-1.5 bg-background px-4 text-[15px] font-semibold hover:bg-accent focus-visible:ring-0 focus-visible:ring-offset-0 sm:h-10 sm:flex-none sm:px-3 sm:text-sm ${
167 isGenerating || disabled ? "opacity-70" : ""
168 }`}
169 >
170 {isGenerating ? (
171 <>
172 <div className="h-4 w-4 animate-spin rounded-full border-2 border-border border-t-primary" />
173 Generating...
174 </>
175 ) : (
176 "Generate Image Slides"
177 )}
178 </Button>
179 </ButtonGroup>
180 );
181 }
182
182 lines Plain Text