├── .gitignore ├── App.tsx ├── README.md ├── components ├── BoardPanel.tsx ├── CanvasSettings.tsx ├── LayerPanel.tsx ├── Loader.tsx ├── PromptBar.tsx ├── QuickPrompts.tsx └── Toolbar.tsx ├── index.html ├── index.tsx ├── metadata.json ├── package.json ├── services └── geminiService.ts ├── src ├── App.tsx └── components │ └── LayerPanel.tsx ├── translations.ts ├── tsconfig.json ├── types.ts ├── utils └── fileUtils.ts └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Group 343 4 | 5 | 6 | # BananaPod | 香蕉铺子 | ZHO 7 | 8 |
9 | 10 | Group 345 11 | 12 | ## 🆕 全新 UI 、iPad/Apple Pencil 手绘支持、视频模式 和 新功能上线! 13 | 14 | 15 | 16 | ### 1)高级质感 UI + 新功能 17 | 18 | Group 378 19 | 20 | ✅局部重绘 21 | 22 | ✅提示词储存/复用系统 23 | 24 | ✅UI 支持高度定制化 25 | 26 | ✅中英双界面 27 | 28 | ✅多画板系统 29 | 30 | ✅图层系统 31 | 32 | ✅图片编辑系统 33 | 34 | ✅图片圆角 35 | 36 | 37 | ### 2)视频生成模式 38 | 39 | https://github.com/user-attachments/assets/ab3742a4-52be-491d-86b3-78607db10d1e 40 | 41 | 42 | ### 3)iPad/Apple Pencil 手绘支持 43 | 44 | Group 407 45 | 46 | Group 405 47 | 48 | 49 | https://github.com/user-attachments/assets/980c2774-62ca-4730-984f-72531b595d5e 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ## 免提示词,内置玩法轻松选,一键构建创意画板 59 | 60 | 我的 Nano Banan 创意玩法大全:[Nano-Banana Creation ZHO](https://github.com/ZHO-ZHO-ZHO/ZHO-nano-banana-Creation) 61 | 62 | 63 | ### 功能主要包含两部分: 64 | 65 | 1)生成/编辑部分:支持多图框选 + 选择玩法直接生成/编辑 66 | 67 | 2)绘制部分,方便标注和手绘图作为输入 68 | 69 | 70 | 71 | https://github.com/user-attachments/assets/83c96432-4246-4c1c-9087-6d0669acdaed 72 | 73 | 74 | 75 | 76 | 与 [香蕉超市|Nano Bananary](https://github.com/ZHO-ZHO-ZHO/Nano-Bananary) 区别: 77 | 78 | **1️⃣ 香蕉铺子|BananaPod** 79 | 80 | 创作白板/画布 81 | 82 | 适合创意专业用户 83 | 84 | 方便多维度生成 构建灵感+创意体系 85 | 86 | 87 | **2️⃣ 香蕉超市|Nano Bananary** 88 | 89 | 窗口式玩法大全 90 | 91 | 适合所有用户 92 | 93 | 方便效果直出+连续编辑 94 | 95 | 96 | # Online 97 | 98 | This contains everything you need to run your app locally. 99 | 100 | View your app in AI Studio: https://ai.studio/apps/drive/1CsvkMqNnxdUrmJZYeSXNZDf6T1Yq2qQW 101 | 102 | ## Run Locally 103 | 104 | **Prerequisites:** Node.js 105 | 106 | 107 | 1. Install dependencies: 108 | `npm install` 109 | 2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key 110 | 3. Run the app: 111 | `npm run dev` 112 | 113 | 114 | 115 | ## 更新日志 116 | 117 | - 20250925 118 | 119 | 全新 UI 、iPad/Apple Pencil 手绘支持、视频模式 和 新功能上线 120 | 121 | ✅局部重绘 122 | 123 | ✅提示词储存/复用系统 124 | 125 | ✅UI 支持高度定制化 126 | 127 | ✅中英双界面 128 | 129 | ✅多画板系统 130 | 131 | ✅图层系统 132 | 133 | ✅图片编辑系统 134 | 135 | ✅图片圆角 136 | 137 | - 20250908 138 | 139 | 创建项目 + 基础功能一步到位 + 内置玩法大全 140 | 141 | 142 | ## Stars 143 | 144 | [![Star History Chart](https://api.star-history.com/svg?repos=ZHO-ZHO-ZHO/BananaPod&type=Date)](https://star-history.com/#ZHO-ZHO-ZHO/BananaPod&Date) 145 | 146 | 147 | ## 关于我 | About me 148 | 149 | 📬 **联系我**: 150 | - 邮箱:zhozho3965@gmail.com 151 | 152 | 153 | 🔗 **社交媒体**: 154 | - 个人页:[-Zho-](https://jike.city/zho) 155 | - Bilibili:[我的B站主页](https://space.bilibili.com/484366804) 156 | - X(Twitter):[我的Twitter](https://twitter.com/ZHO_ZHO_ZHO) 157 | - 小红书:[我的小红书主页](https://www.xiaohongshu.com/user/profile/63f11530000000001001e0c8?xhsshare=CopyLink&appuid=63f11530000000001001e0c8&apptime=1690528872) 158 | 159 | 💡 **支持我**: 160 | - B站:[B站充电](https://space.bilibili.com/484366804) 161 | - 爱发电:[为我充电](https://afdian.com/a/ZHOZHO) 162 | 163 | 164 | ## Credits 165 | 166 | [Gemini 2.5 Flash Image](https://gemini.google.com/app) 167 | -------------------------------------------------------------------------------- /components/BoardPanel.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { useState, useRef, useEffect } from 'react'; 3 | import type { Board } from '../types'; 4 | 5 | interface BoardPanelProps { 6 | isOpen: boolean; 7 | onClose: () => void; 8 | boards: Board[]; 9 | activeBoardId: string; 10 | onSwitchBoard: (id: string) => void; 11 | onAddBoard: () => void; 12 | onRenameBoard: (id: string, name: string) => void; 13 | onDuplicateBoard: (id: string) => void; 14 | onDeleteBoard: (id: string) => void; 15 | generateBoardThumbnail: (elements: Board['elements']) => string; 16 | } 17 | 18 | const BoardItem: React.FC<{ 19 | board: Board; 20 | isActive: boolean; 21 | thumbnail: string; 22 | onClick: () => void; 23 | onRename: (name: string) => void; 24 | onDuplicate: () => void; 25 | onDelete: () => void; 26 | }> = ({ board, isActive, thumbnail, onClick, onRename, onDuplicate, onDelete }) => { 27 | const [isEditing, setIsEditing] = useState(false); 28 | const [name, setName] = useState(board.name); 29 | const [menuOpen, setMenuOpen] = useState(false); 30 | const inputRef = useRef(null); 31 | const menuRef = useRef(null); 32 | 33 | useEffect(() => { 34 | setName(board.name); 35 | }, [board.name]); 36 | 37 | useEffect(() => { 38 | if (isEditing && inputRef.current) { 39 | inputRef.current.focus(); 40 | inputRef.current.select(); 41 | } 42 | }, [isEditing]); 43 | 44 | useEffect(() => { 45 | const handleClickOutside = (event: MouseEvent) => { 46 | if (menuRef.current && !menuRef.current.contains(event.target as Node)) { 47 | setMenuOpen(false); 48 | } 49 | }; 50 | document.addEventListener('mousedown', handleClickOutside); 51 | return () => document.removeEventListener('mousedown', handleClickOutside); 52 | }, []); 53 | 54 | const handleBlur = () => { 55 | setIsEditing(false); 56 | if (name.trim() === '') { 57 | setName(board.name); 58 | } else if (name.trim() !== board.name) { 59 | onRename(name.trim()); 60 | } 61 | }; 62 | 63 | const handleMenuAction = (action: 'rename' | 'duplicate' | 'delete') => { 64 | setMenuOpen(false); 65 | switch (action) { 66 | case 'rename': 67 | setIsEditing(true); 68 | break; 69 | case 'duplicate': 70 | onDuplicate(); 71 | break; 72 | case 'delete': 73 | if (window.confirm(`Are you sure you want to delete "${board.name}"?`)) { 74 | onDelete(); 75 | } 76 | break; 77 | } 78 | }; 79 | 80 | return ( 81 |
85 |
86 | {`${board.name} 87 |
88 |
89 | {isEditing ? ( 90 | setName(e.target.value)} 95 | onBlur={handleBlur} 96 | onKeyDown={(e) => e.key === 'Enter' && handleBlur()} 97 | className="w-full bg-transparent border-b border-blue-400 outline-none text-white text-sm" 98 | onClick={e => e.stopPropagation()} 99 | /> 100 | ) : ( 101 | setIsEditing(true)}>{board.name} 102 | )} 103 | 104 |
105 | 111 | {menuOpen && ( 112 |
113 | 114 | 115 |
116 | 117 |
118 | )} 119 |
120 |
121 |
122 | ); 123 | }; 124 | 125 | 126 | export const BoardPanel: React.FC = ({ 127 | isOpen, onClose, boards, activeBoardId, onSwitchBoard, onAddBoard, 128 | onRenameBoard, onDuplicateBoard, onDeleteBoard, generateBoardThumbnail 129 | }) => { 130 | if (!isOpen) return null; 131 | 132 | return ( 133 |
137 |
138 |

Boards

139 |
140 | 143 | 146 |
147 |
148 |
149 | {boards.map(board => ( 150 | onSwitchBoard(board.id)} 156 | onRename={(name) => onRenameBoard(board.id, name)} 157 | onDuplicate={() => onDuplicateBoard(board.id)} 158 | onDelete={() => onDeleteBoard(board.id)} 159 | /> 160 | ))} 161 |
162 |
163 | ); 164 | }; 165 | -------------------------------------------------------------------------------- /components/CanvasSettings.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react'; 4 | import type { WheelAction } from '../types'; 5 | 6 | interface CanvasSettingsProps { 7 | isOpen: boolean; 8 | onClose: () => void; 9 | canvasBackgroundColor: string; 10 | onCanvasBackgroundColorChange: (color: string) => void; 11 | language: 'en' | 'zho'; 12 | setLanguage: (lang: 'en' | 'zho') => void; 13 | uiTheme: { color: string; opacity: number }; 14 | setUiTheme: (theme: { color: string; opacity: number }) => void; 15 | buttonTheme: { color: string; opacity: number }; 16 | setButtonTheme: (theme: { color: string; opacity: number }) => void; 17 | wheelAction: WheelAction; 18 | setWheelAction: (action: WheelAction) => void; 19 | t: (key: string) => string; 20 | } 21 | 22 | export const CanvasSettings: React.FC = ({ 23 | isOpen, 24 | onClose, 25 | canvasBackgroundColor, 26 | onCanvasBackgroundColorChange, 27 | language, 28 | setLanguage, 29 | uiTheme, 30 | setUiTheme, 31 | buttonTheme, 32 | setButtonTheme, 33 | wheelAction, 34 | setWheelAction, 35 | t 36 | }) => { 37 | if (!isOpen) return null; 38 | 39 | return ( 40 |
44 |
e.stopPropagation()} 48 | > 49 |
50 |

{t('settings.title')}

51 | 54 |
55 | 56 |
57 | 58 | {/* Language Settings */} 59 |
60 | 61 |
62 | 68 | 74 |
75 |
76 | 77 | {/* UI Theme Settings */} 78 |
79 |

{t('settings.uiTheme')}

80 |
81 | 82 | setUiTheme({ ...uiTheme, color: e.target.value })} 87 | className="w-8 h-8 p-0 border-none rounded-md cursor-pointer bg-transparent" 88 | /> 89 |
90 |
91 | 92 | setUiTheme({ ...uiTheme, opacity: parseFloat(e.target.value) })} 100 | className="w-32" 101 | /> 102 | {Math.round(uiTheme.opacity * 100)}% 103 |
104 |
105 | 106 |
107 | 108 | {/* Button Theme Settings */} 109 |
110 |

{t('settings.actionButtonsTheme')}

111 |
112 | 113 | setButtonTheme({ ...buttonTheme, color: e.target.value })} 118 | className="w-8 h-8 p-0 border-none rounded-md cursor-pointer bg-transparent" 119 | /> 120 |
121 |
122 | 123 | setButtonTheme({ ...buttonTheme, opacity: parseFloat(e.target.value) })} 131 | className="w-32" 132 | /> 133 | {Math.round(buttonTheme.opacity * 100)}% 134 |
135 |
136 | 137 |
138 | 139 | {/* Mouse Wheel Settings */} 140 |
141 | 142 |
143 | 149 | 155 |
156 |
157 | 158 | 159 | {/* Canvas Settings */} 160 |
161 |

{t('settings.canvas')}

162 |
163 | 164 | onCanvasBackgroundColorChange(e.target.value)} 169 | className="w-8 h-8 p-0 border-none rounded-md cursor-pointer bg-transparent" 170 | /> 171 |
172 |
173 |
174 |
175 | ); 176 | }; -------------------------------------------------------------------------------- /components/LayerPanel.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import React, { useState, useRef, useEffect, useMemo } from 'react'; 5 | import type { Element } from '../types'; 6 | 7 | interface LayerPanelProps { 8 | isOpen: boolean; 9 | onClose: () => void; 10 | elements: Element[]; 11 | selectedElementIds: string[]; 12 | onSelectElement: (id: string | null) => void; 13 | onToggleVisibility: (id: string) => void; 14 | onToggleLock: (id: string) => void; 15 | onRenameElement: (id: string, name: string) => void; 16 | onReorder: (draggedId: string, targetId: string, position: 'before' | 'after') => void; 17 | } 18 | 19 | const getElementIcon = (element: Element): React.ReactNode => { 20 | const commonProps = { 21 | width: "16", 22 | height: "16", 23 | viewBox: "0 0 24 24", 24 | fill: "none", 25 | stroke: "currentColor", 26 | strokeWidth: "2", 27 | strokeLinecap: "round" as const, 28 | strokeLinejoin: "round" as const, 29 | }; 30 | 31 | switch (element.type) { 32 | case 'image': 33 | return ; 34 | case 'video': 35 | return ; 36 | case 'text': 37 | return ; 38 | case 'shape': 39 | switch (element.shapeType) { 40 | case 'rectangle': return ; 41 | case 'circle': return ; 42 | case 'triangle': return ; 43 | } 44 | break; 45 | case 'group': 46 | return ; 47 | case 'path': 48 | return ; 49 | case 'arrow': 50 | return ; 51 | case 'line': 52 | return ; 53 | default: 54 | return ; 55 | } 56 | return ; 57 | }; 58 | 59 | const LayerItem: React.FC<{ 60 | element: Element; 61 | level: number; 62 | isSelected: boolean; 63 | onSelect: () => void; 64 | onToggleVisibility: () => void; 65 | onToggleLock: () => void; 66 | onRename: (name: string) => void; 67 | onDragStart: (e: React.DragEvent) => void; 68 | onDragOver: (e: React.DragEvent) => void; 69 | onDrop: (e: React.DragEvent) => void; 70 | onDragLeave: (e: React.DragEvent) => void; 71 | }> = ({ element, level, isSelected, onSelect, onToggleVisibility, onToggleLock, onRename, ...dragProps }) => { 72 | const [isEditing, setIsEditing] = useState(false); 73 | const [name, setName] = useState(element.name || element.type); 74 | const inputRef = useRef(null); 75 | 76 | useEffect(() => { 77 | setName(element.name || element.type); 78 | }, [element.name, element.type]); 79 | 80 | useEffect(() => { 81 | if (isEditing && inputRef.current) { 82 | inputRef.current.focus(); 83 | inputRef.current.select(); 84 | } 85 | }, [isEditing]); 86 | 87 | const handleBlur = () => { 88 | setIsEditing(false); 89 | if (name.trim() === '') { 90 | setName(element.name || element.type); 91 | } else { 92 | onRename(name); 93 | } 94 | }; 95 | 96 | const iconProps = { 97 | width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", 98 | stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" as const, strokeLinejoin: "round" as const 99 | }; 100 | 101 | return ( 102 |
setIsEditing(true)} 107 | className={`flex items-center space-x-2 p-1.5 rounded-md cursor-pointer text-sm transition-colors group ${ 108 | isSelected ? 'bg-blue-500/30' : 'hover:bg-white/10' 109 | } ${element.isVisible === false ? 'opacity-50' : ''}`} 110 | style={{ paddingLeft: `${10 + level * 20}px` }} 111 | > 112 | {getElementIcon(element)} 113 | {isEditing ? ( 114 | setName(e.target.value)} 119 | onBlur={handleBlur} 120 | onKeyDown={(e) => e.key === 'Enter' && handleBlur()} 121 | className="flex-grow bg-transparent border-b border-blue-400 outline-none text-white" 122 | onClick={e => e.stopPropagation()} 123 | /> 124 | ) : ( 125 | {name} 126 | )} 127 |
128 | 138 | 148 |
149 |
150 | ); 151 | }; 152 | 153 | export const LayerPanel: React.FC = ({ isOpen, onClose, elements, selectedElementIds, onSelectElement, onToggleVisibility, onToggleLock, onRenameElement, onReorder }) => { 154 | const panelRef = useRef(null); 155 | const [dragOverId, setDragOverId] = useState(null); 156 | 157 | const handleDragStart = (e: React.DragEvent, id: string) => { 158 | e.dataTransfer.setData('text/plain', id); 159 | e.dataTransfer.effectAllowed = 'move'; 160 | }; 161 | 162 | const handleDragOver = (e: React.DragEvent) => { 163 | e.preventDefault(); 164 | const target = e.currentTarget; 165 | const id = target.getAttribute('data-id'); 166 | setDragOverId(id); 167 | target.style.background = 'rgba(255,255,255,0.2)'; 168 | }; 169 | 170 | const handleDragLeave = (e: React.DragEvent) => { 171 | e.currentTarget.style.background = ''; 172 | setDragOverId(null); 173 | }; 174 | 175 | const handleDrop = (e: React.DragEvent, targetId: string) => { 176 | e.preventDefault(); 177 | e.currentTarget.style.background = ''; 178 | setDragOverId(null); 179 | const draggedId = e.dataTransfer.getData('text/plain'); 180 | 181 | const rect = e.currentTarget.getBoundingClientRect(); 182 | const position = e.clientY - rect.top > rect.height / 2 ? 'after' : 'before'; 183 | 184 | if (draggedId && targetId && draggedId !== targetId) { 185 | onReorder(draggedId, targetId, position); 186 | } 187 | }; 188 | 189 | const elementMap = useMemo(() => new Map(elements.map(el => [el.id, el])), [elements]); 190 | const rootElements = useMemo(() => elements.filter(el => !el.parentId), [elements]); 191 | 192 | const renderLayers = (elementIds: string[], level: number) => { 193 | return elementIds.map(id => { 194 | const element = elementMap.get(id); 195 | if (!element) return null; 196 | 197 | const childrenIds = elements.filter(el => el.parentId === id).map(el => el.id); 198 | 199 | return ( 200 | 201 |
handleDrop(e, id)}> 202 | onSelectElement(id)} 207 | onToggleLock={() => onToggleLock(id)} 208 | onToggleVisibility={() => onToggleVisibility(id)} 209 | onRename={name => onRenameElement(id, name)} 210 | onDragStart={e => handleDragStart(e, id)} 211 | onDragOver={handleDragOver} 212 | onDragLeave={handleDragLeave} 213 | onDrop={(e) => handleDrop(e, id)} 214 | /> 215 |
216 | {childrenIds.length > 0 && renderLayers(childrenIds, level + 1)} 217 |
218 | ); 219 | }); 220 | }; 221 | 222 | // Render elements in their actual array order for Z-index representation 223 | const renderOrderedLayers = (elements: Element[], level: number = 0, parentId?: string) => { 224 | return elements 225 | .filter(el => el.parentId === parentId) 226 | .map(element => ( 227 | 228 |
handleDrop(e, element.id)}> 229 | onSelectElement(element.id)} 234 | onToggleLock={() => onToggleLock(element.id)} 235 | onToggleVisibility={() => onToggleVisibility(element.id)} 236 | onRename={name => onRenameElement(element.id, name)} 237 | onDragStart={e => handleDragStart(e, element.id)} 238 | onDragOver={handleDragOver} 239 | onDragLeave={handleDragLeave} 240 | onDrop={(e) => handleDrop(e, element.id)} 241 | /> 242 |
243 | {renderOrderedLayers(elements, level + 1, element.id)} 244 |
245 | )); 246 | }; 247 | 248 | 249 | if (!isOpen) return null; 250 | 251 | return ( 252 |
257 |
258 |

Layers

259 | 262 |
263 |
264 | {renderOrderedLayers([...elements].reverse())} 265 |
266 |
267 | ); 268 | }; 269 | -------------------------------------------------------------------------------- /components/Loader.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react'; 4 | 5 | interface LoaderProps { 6 | progressMessage: string; 7 | } 8 | 9 | export const Loader: React.FC = ({ progressMessage }) => { 10 | return ( 11 |
12 |
13 | 14 | 15 | 16 | 17 | {progressMessage || 'Generating...'} 18 |
19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /components/PromptBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { QuickPrompts } from './QuickPrompts'; 3 | import type { UserEffect, GenerationMode } from '../types'; 4 | 5 | interface PromptBarProps { 6 | t: (key: string, ...args: any[]) => string; 7 | prompt: string; 8 | setPrompt: (prompt: string) => void; 9 | onGenerate: () => void; 10 | isLoading: boolean; 11 | isSelectionActive: boolean; 12 | selectedElementCount: number; 13 | userEffects: UserEffect[]; 14 | onAddUserEffect: (effect: UserEffect) => void; 15 | onDeleteUserEffect: (id: string) => void; 16 | generationMode: GenerationMode; 17 | setGenerationMode: (mode: GenerationMode) => void; 18 | videoAspectRatio: '16:9' | '9:16'; 19 | setVideoAspectRatio: (ratio: '16:9' | '9:16') => void; 20 | } 21 | 22 | export const PromptBar: React.FC = ({ 23 | t, 24 | prompt, 25 | setPrompt, 26 | onGenerate, 27 | isLoading, 28 | isSelectionActive, 29 | selectedElementCount, 30 | userEffects, 31 | onAddUserEffect, 32 | onDeleteUserEffect, 33 | generationMode, 34 | setGenerationMode, 35 | videoAspectRatio, 36 | setVideoAspectRatio, 37 | }) => { 38 | const textareaRef = React.useRef(null); 39 | 40 | React.useEffect(() => { 41 | if (textareaRef.current) { 42 | textareaRef.current.style.height = 'auto'; // Reset height 43 | textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; 44 | } 45 | }, [prompt]); 46 | 47 | const getPlaceholderText = () => { 48 | if (!isSelectionActive) { 49 | return generationMode === 'video' ? t('promptBar.placeholderDefaultVideo') : t('promptBar.placeholderDefault'); 50 | } 51 | if (selectedElementCount === 1) { 52 | return t('promptBar.placeholderSingle'); 53 | } 54 | return t('promptBar.placeholderMultiple', selectedElementCount); 55 | }; 56 | 57 | const handleKeyDown = (e: React.KeyboardEvent) => { 58 | if (e.key === 'Enter' && !e.shiftKey) { 59 | e.preventDefault(); 60 | if (!isLoading && prompt.trim()) { 61 | onGenerate(); 62 | } 63 | } 64 | }; 65 | 66 | const handleSaveEffect = () => { 67 | const name = window.prompt(t('myEffects.saveEffectPrompt'), t('myEffects.defaultName')); 68 | if (name && prompt.trim()) { 69 | onAddUserEffect({ id: `user_${Date.now()}`, name, value: prompt }); 70 | } 71 | }; 72 | 73 | const containerStyle: React.CSSProperties = { 74 | backgroundColor: `var(--ui-bg-color)`, 75 | }; 76 | 77 | return ( 78 |
79 |
83 |
84 | 85 | 86 |
87 | 88 | {generationMode === 'video' && ( 89 |
90 | 93 | 96 |
97 | )} 98 | 105 |