diff --git a/README.md b/README.md index 5b88f2d..98bdac2 100644 --- a/README.md +++ b/README.md @@ -1,226 +1,226 @@ -# 🍌 Nano Banana AI Image Editor -Release Version: (v1.0) +# 🍌 Nano Banana AI 图像编辑器 +发布版本: (v1.0) -### **⏬ Get Your 1-Click Install Copy!** -Join the [Vibe Coding is Life Skool Community](https://www.skool.com/vibe-coding-is-life/about?ref=456537abaf37491cbcc6976f3c26af41) and get a **1-click ⚡Bolt.new installation clone** of this app, plus access to live build sessions, exclusive project downloads, AI prompts, masterclasses, and the best vibe coding community on the web! +### **⏬ 获取一键安装副本!** +加入 [Vibe Coding is Life Skool 社区](https://www.skool.com/vibe-coding-is-life/about?ref=456537abaf37491cbcc6976f3c26af41) 获取此应用的 **一键 ⚡Bolt.new 安装克隆**,以及现场构建会话、独家项目下载、AI 提示、大师课程和网络上最好的氛围编码社区的访问权限! --- -**Professional AI Image Generation & Conversational Editing Platform** +**专业的 AI 图像生成和对话式编辑平台** -A production-ready React + TypeScript application for delightful image generation and conversational, region-aware revisions using Google's Gemini 2.5 Flash Image model. Built with modern web technologies and designed for both creators and developers. +一个生产就绪的 React + TypeScript 应用程序,用于愉快的图像生成和使用 Google Gemini 2.5 Flash Image 模型进行对话式、区域感知的修改。采用现代网络技术构建,专为创作者和开发者设计。 -[![Nano Banana Image Editor](https://getsmartgpt.com/nano-banana-editor.jpg)](https://nanobananaeditor.dev) +[![Nano Banana 图像编辑器](https://getsmartgpt.com/nano-banana-editor.jpg)](https://nanobananaeditor.dev) -🍌 [Try the LIVE Demo](https://nanobananaeditor.dev) +🍌 [试用在线演示](https://nanobananaeditor.dev) -## ✨ Key Features +## ✨ 主要功能 -### 🎨 **AI-Powered Creation** -- **Text-to-Image Generation** - Create stunning images from descriptive prompts -- **Live Quality Tips** - Real-time feedback to improve your prompts -- **Reference Image Support** - Use up to 2 reference images to guide generation -- **Advanced Controls** - Fine-tune creativity levels and use custom seeds +### 🎨 **AI 驱动的创作** +- **文本到图像生成** - 从描述性提示创建令人惊叹的图像 +- **实时质量提示** - 实时反馈以改进您的提示 +- **参考图像支持** - 使用最多 2 张参考图像指导生成 +- **高级控制** - 微调创意水平并使用自定义种子 -### ✏️ **Intelligent Editing** -- **Conversational Editing** - Modify images using natural language instructions -- **Region-Aware Selection** - Paint masks to target specific areas for editing -- **Style Reference Images** - Upload reference images to guide editing style -- **Non-Destructive Workflow** - All edits preserve the original image +### ✏️ **智能编辑** +- **对话式编辑** - 使用自然语言指令修改图像 +- **区域感知选择** - 绘制遮罩以针对特定区域进行编辑 +- **样式参考图像** - 上传参考图像以指导编辑样式 +- **非破坏性工作流** - 所有编辑都保留原始图像 -### 🖼️ **Professional Canvas** -- **Interactive Canvas** - Zoom, pan, and navigate large images smoothly -- **Brush Tools** - Variable brush sizes for precise mask painting -- **Mobile Optimized** - Responsive design that works beautifully on all devices -- **Keyboard Shortcuts** - Efficient workflow with hotkeys +### 🖼️ **专业画布** +- **交互式画布** - 平滑缩放、平移和导航大图像 +- **画笔工具** - 可变画笔尺寸以实现精确的遮罩绘制 +- **移动设备优化** - 响应式设计,在所有设备上都能完美运行 +- **键盘快捷键** - 使用热键提高工作效率 -### 📚 **Project Management** -- **Generation History** - Track all your creations and edits -- **Variant Comparison** - Generate and compare multiple versions side-by-side -- **Full Undo/Redo** - Complete generation tree with branching history -- **Asset Management** - Organized storage of all generated content +### 📚 **项目管理** +- **生成历史** - 跟踪所有创作和编辑 +- **变体比较** - 生成并并排比较多个版本 +- **完整撤销/重做** - 具有分支历史的完整生成树 +- **资产管理** - 有序存储所有生成的内容 -### 🔒 **Enterprise Features** -- **SynthID Watermarking** - Built-in AI provenance with invisible watermarks -- **Offline Caching** - IndexedDB storage for offline asset access -- **Type Safety** - Full TypeScript implementation with strict typing -- **Performance Optimized** - React Query for efficient state management +### 🔒 **企业功能** +- **SynthID 水印** - 内置 AI 来源追踪和隐形水印 +- **离线缓存** - IndexedDB 存储以实现离线资产访问 +- **类型安全** - 完整的 TypeScript 实现和严格类型检查 +- **性能优化** - React Query 实现高效状态管理 -## 🚀 Quick Start +## 🚀 快速开始 -### Prerequisites +### 先决条件 - Node.js 18+ -- A [Google AI Studio](https://aistudio.google.com/) API key +- 一个 [Google AI Studio](https://aistudio.google.com/) API 密钥 -### Installation +### 安装 -1. **Clone and install dependencies**: +1. **克隆并安装依赖**: ```bash git clone cd nano-banana-image-editor npm install ``` -2. **Configure environment**: +2. **配置环境**: ```bash cp .env.example .env - # Add your Gemini API key to VITE_GEMINI_API_KEY + # 将您的 Gemini API 密钥添加到 VITE_GEMINI_API_KEY ``` -3. **Start development server**: +3. **启动开发服务器**: ```bash npm run dev ``` -4. **Open in browser**: Navigate to `http://localhost:5173` +4. **在浏览器中打开**: 导航到 `http://localhost:5173` -## 🎯 Usage Guide +## 🎯 使用指南 -### Creating Images -1. Select **Generate** mode -2. Write a detailed prompt describing your desired image -3. Optionally upload reference images (max 2) -4. Adjust creativity settings if needed -5. Click **Generate** or press `Cmd/Ctrl + Enter` +### 创建图像 +1. 选择 **生成** 模式 +2. 编写详细提示描述您想要的图像 +3. 可选上传参考图像(最多 2 张) +4. 如需要调整创意设置 +5. 点击 **生成** 或按 `Cmd/Ctrl + Enter` -### Editing Images -1. Switch to **Edit** mode -2. Upload an image or use a previously generated one -3. Optionally paint a mask to target specific areas -4. Describe your desired changes in natural language -5. Click **Apply Edit** to see the results +### 编辑图像 +1. 切换到 **编辑** 模式 +2. 上传图像或使用先前生成的图像 +3. 可选绘制遮罩以针对特定区域 +4. 用自然语言描述您想要的更改 +5. 点击 **应用编辑** 查看结果 -### Advanced Workflows -- Use **Select** mode to paint precise masks for targeted edits -- Compare variants in the History panel -- Download high-quality PNG outputs -- Use keyboard shortcuts for efficient navigation +### 高级工作流 +- 使用 **选择** 模式绘制精确遮罩以进行目标编辑 +- 在历史面板中比较变体 +- 下载高质量 PNG 输出 +- 使用键盘快捷键进行高效导航 -## ⌨️ Keyboard Shortcuts +## ⌨️ 键盘快捷键 -| Shortcut | Action | +| 快捷键 | 操作 | |----------|--------| -| `Cmd/Ctrl + Enter` | Generate/Apply Edit | -| `Shift + R` | Re-roll variants | -| `E` | Switch to Edit mode | -| `G` | Switch to Generate mode | -| `M` | Switch to Select mode | -| `H` | Toggle history panel | -| `P` | Toggle prompt panel | +| `Cmd/Ctrl + Enter` | 生成/应用编辑 | +| `Shift + R` | 重新生成变体 | +| `E` | 切换到编辑模式 | +| `G` | 切换到生成模式 | +| `M` | 切换到选择模式 | +| `H` | 切换历史面板 | +| `P` | 切换提示面板 | -## 🏗️ Architecture +## 🏗️ 架构 -### Tech Stack -- **Frontend**: React 18, TypeScript, Tailwind CSS -- **State Management**: Zustand for app state, React Query for server state -- **Canvas**: Konva.js for interactive image display and mask overlays -- **AI Integration**: Google Generative AI SDK (Gemini 2.5 Flash Image) -- **Storage**: IndexedDB for offline asset caching -- **Build Tool**: Vite for fast development and optimized builds +### 技术栈 +- **前端**: React 18, TypeScript, Tailwind CSS +- **状态管理**: Zustand 用于应用状态, React Query 用于服务器状态 +- **画布**: Konva.js 用于交互式图像显示和遮罩叠加 +- **AI 集成**: Google Generative AI SDK (Gemini 2.5 Flash Image) +- **存储**: IndexedDB 用于离线资产缓存 +- **构建工具**: Vite 用于快速开发和优化构建 -### Project Structure +### 项目结构 ``` src/ -├── components/ # React components -│ ├── ui/ # Reusable UI components (Button, Input, etc.) -│ ├── PromptComposer.tsx # Prompt input and tool selection -│ ├── ImageCanvas.tsx # Interactive canvas with Konva -│ ├── HistoryPanel.tsx # Generation history and variants -│ ├── Header.tsx # App header and navigation -│ └── InfoModal.tsx # About modal with links -├── services/ # External service integrations -│ ├── geminiService.ts # Gemini API client -│ ├── cacheService.ts # IndexedDB caching layer -│ └── imageProcessing.ts # Image manipulation utilities -├── store/ # Zustand state management -│ └── useAppStore.ts # Global application state -├── hooks/ # Custom React hooks -│ ├── useImageGeneration.ts # Generation and editing logic -│ └── useKeyboardShortcuts.ts # Keyboard navigation -├── utils/ # Utility functions -│ ├── cn.ts # Class name utility -│ └── imageUtils.ts # Image processing helpers -└── types/ # TypeScript type definitions - └── index.ts # Core type definitions +├── components/ # React 组件 +│ ├── ui/ # 可重用的 UI 组件 (Button, Input, 等) +│ ├── PromptComposer.tsx # 提示输入和工具选择 +│ ├── ImageCanvas.tsx # 使用 Konva 的交互式画布 +│ ├── HistoryPanel.tsx # 生成历史和变体 +│ ├── Header.tsx # 应用头部和导航 +│ └── InfoModal.tsx # 关于模态框和链接 +├── services/ # 外部服务集成 +│ ├── geminiService.ts # Gemini API 客户端 +│ ├── cacheService.ts # IndexedDB 缓存层 +│ └── imageProcessing.ts # 图像处理工具 +├── store/ # Zustand 状态管理 +│ └── useAppStore.ts # 全局应用状态 +├── hooks/ # 自定义 React 钩子 +│ ├── useImageGeneration.ts # 生成和编辑逻辑 +│ └── useKeyboardShortcuts.ts # 键盘导航 +├── utils/ # 工具函数 +│ ├── cn.ts # 类名工具 +│ └── imageUtils.ts # 图像处理助手 +└── types/ # TypeScript 类型定义 + └── index.ts # 核心类型定义 ``` -## 🔧 Configuration +## 🔧 配置 -### Environment Variables +### 环境变量 ```bash VITE_GEMINI_API_KEY=your_gemini_api_key_here ``` -### Model Configuration -- **Model**: `gemini-2.5-flash-image-preview` -- **Output Format**: 1024×1024 PNG with SynthID watermarks -- **Input Formats**: PNG, JPEG, WebP -- **Temperature Range**: 0-1 (0 = deterministic, 1 = creative) +### 模型配置 +- **模型**: `gemini-2.5-flash-image-preview` +- **输出格式**: 1024×1024 PNG 带 SynthID 水印 +- **输入格式**: PNG, JPEG, WebP +- **温度范围**: 0-1 (0 = 确定性, 1 = 创意) -## 🚀 Deployment +## 🚀 部署 -### Development +### 开发 ```bash -npm run dev # Start development server -npm run build # Build for production -npm run preview # Preview production build -npm run lint # Run ESLint +npm run dev # 启动开发服务器 +npm run build # 构建生产版本 +npm run preview # 预览生产构建 +npm run lint # 运行 ESLint ``` -### Production Considerations -- **API Security**: Implement backend proxy for API calls in production -- **Rate Limiting**: Add proper rate limiting and usage quotas -- **Authentication**: Consider user authentication for multi-user deployments -- **Storage**: Set up cloud storage for generated assets -- **Monitoring**: Add error tracking and analytics +### 生产考虑 +- **API 安全**: 在生产环境中为 API 调用实现后端代理 +- **速率限制**: 添加适当的速率限制和使用配额 +- **身份验证**: 考虑为多用户部署添加用户身份验证 +- **存储**: 设置云存储以存储生成的资产 +- **监控**: 添加错误跟踪和分析 -## 📄 License & Copyright +## 📄 许可证和版权 -**Copyright © 2025 [Mark Fulton](https://markfulton.com)** +**版权所有 © 2025 [Mark Fulton](https://markfulton.com)** -This project is licensed under the **GNU Affero General Public License v3.0** (AGPL-3.0). +该项目根据 **GNU Affero 通用公共许可证 v3.0** (AGPL-3.0) 授权。 -### What this means: -- ✅ **Free to use** for personal and commercial projects -- ✅ **Modify and distribute** with proper attribution -- ⚠️ **Share modifications** - Any changes must be shared under the same license -- ⚠️ **Network use** - If you run this as a web service, you must provide source code +### 这意味着: +- ✅ **免费使用** 于个人和商业项目 +- ✅ **修改和分发** 需要适当署名 +- ⚠️ **分享修改** - 任何更改必须在相同许可证下共享 +- ⚠️ **网络使用** - 如果您将其作为网络服务运行,必须提供源代码 -See the [LICENSE](LICENSE) file for full details. +有关完整详情,请参见 [LICENSE](LICENSE) 文件。 -## 🤝 Contributing +## 🤝 贡献 -We welcome contributions! Please: +我们欢迎贡献!请: -1. **Follow the established patterns** - Keep components under 200 lines -2. **Maintain type safety** - Use TypeScript strictly with proper definitions -3. **Test thoroughly** - Ensure keyboard navigation and accessibility -4. **Document changes** - Update README and add inline comments -5. **Respect the license** - All contributions will be under AGPL-3.0 +1. **遵循既定模式** - 保持组件在 200 行以内 +2. **维护类型安全** - 严格使用 TypeScript 和正确定义 +3. **彻底测试** - 确保键盘导航和可访问性 +4. **记录更改** - 更新 README 并添加内联注释 +5. **遵守许可证** - 所有贡献都将遵循 AGPL-3.0 -## 🔗 Links & Resources +## 🔗 链接和资源 -- **Creator**: [Mark Fulton](https://markfulton.com) -- **AI Training Program**: [Reinventing.AI](https://www.reinventing.ai/) -- **Community**: [Vibe Coding is Life Skool](https://www.skool.com/vibe-coding-is-life/about?ref=456537abaf37491cbcc6976f3c26af41) -- **Google AI Studio**: [Get your API key](https://aistudio.google.com/) -- **Gemini API Docs**: [Official Documentation](https://ai.google.dev/gemini-api/docs) +- **创建者**: [Mark Fulton](https://markfulton.com) +- **AI 培训计划**: [Reinventing.AI](https://www.reinventing.ai/) +- **社区**: [Vibe Coding is Life Skool](https://www.skool.com/vibe-coding-is-life/about?ref=456537abaf37491cbcc6976f3c26af41) +- **Google AI Studio**: [获取您的 API 密钥](https://aistudio.google.com/) +- **Gemini API 文档**: [官方文档](https://ai.google.dev/gemini-api/docs) -## 🐛 Known Issues & Limitations +## 🐛 已知问题和限制 -- **Client-side API calls** - Currently uses direct API calls (implement backend proxy for production) -- **Browser compatibility** - Requires modern browsers with Canvas and WebGL support -- **Rate limits** - Subject to Google AI Studio rate limits -- **Image size** - Optimized for 1024×1024 outputs (Gemini model output dimensions may vary) +- **客户端 API 调用** - 目前使用直接 API 调用(在生产环境中实现后端代理) +- **浏览器兼容性** - 需要支持 Canvas 和 WebGL 的现代浏览器 +- **速率限制** - 受 Google AI Studio 速率限制约束 +- **图像尺寸** - 针对 1024×1024 输出进行了优化(Gemini 模型输出尺寸可能有所不同) -## 🎯 Suggested Updates +## 🎯 建议更新 -- [ ] Backend API proxy implementation -- [ ] User authentication and project sharing -- [ ] Advanced brush tools and selection methods -- [ ] Plugin system for custom filters -- [ ] Integration with cloud storage providers +- [ ] 后端 API 代理实现 +- [ ] 用户身份验证和项目共享 +- [ ] 高级画笔工具和选择方法 +- [ ] 自定义过滤器的插件系统 +- [ ] 与云存储提供商集成 --- -**Built by [Mark Fulton](https://markfulton.com)** | **Powered by Gemini 2.5 Flash Image** | **Made with Bolt.new** +**由 [Mark Fulton](https://markfulton.com) 构建** | **由 Gemini 2.5 Flash Image 提供支持** | **使用 Bolt.new 制作** diff --git a/src/App.tsx b/src/App.tsx index 289ec68..3c8b01e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { ImageCanvas } from './components/ImageCanvas'; import { HistoryPanel } from './components/HistoryPanel'; import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; import { useAppStore } from './store/useAppStore'; +import { ToastProvider } from './components/ToastContext'; const queryClient = new QueryClient({ defaultOptions: { @@ -59,7 +60,9 @@ function AppContent() { function App() { return ( - + + + ); } diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx new file mode 100644 index 0000000..093d2d3 --- /dev/null +++ b/src/components/Toast.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { cn } from '../utils/cn'; +import { X } from 'lucide-react'; + +export interface ToastProps { + id: string; + message: string; + type: 'success' | 'error' | 'warning' | 'info'; + onClose: (id: string) => void; +} + +export const Toast: React.FC = ({ id, message, type, onClose }) => { + const getTypeStyles = () => { + switch (type) { + case 'success': + return 'bg-green-500 text-white'; + case 'error': + return 'bg-red-500 text-white'; + case 'warning': + return 'bg-yellow-500 text-white'; + case 'info': + return 'bg-blue-500 text-white'; + default: + return 'bg-gray-500 text-white'; + } + }; + + return ( +
+ {message} + +
+ ); +}; \ No newline at end of file diff --git a/src/components/ToastContext.tsx b/src/components/ToastContext.tsx new file mode 100644 index 0000000..cdb056a --- /dev/null +++ b/src/components/ToastContext.tsx @@ -0,0 +1,87 @@ +import React, { createContext, useContext, useReducer, useState, useEffect } from 'react'; +import { Toast } from './Toast'; + +type ToastType = 'success' | 'error' | 'warning' | 'info'; + +export interface ToastMessage { + id: string; + message: string; + type: ToastType; + duration?: number; +} + +export interface ToastContextType { + addToast: (message: string, type: ToastType, duration?: number) => void; + removeToast: (id: string) => void; +} + +const ToastContext = createContext(undefined); + +type ToastAction = + | { type: 'ADD_TOAST'; payload: ToastMessage } + | { type: 'REMOVE_TOAST'; payload: string }; + +const toastReducer = (state: ToastMessage[], action: ToastAction): ToastMessage[] => { + switch (action.type) { + case 'ADD_TOAST': + return [...state, action.payload]; + case 'REMOVE_TOAST': + return state.filter(toast => toast.id !== action.payload); + default: + return state; + } +}; + +export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [toasts, dispatch] = useReducer(toastReducer, []); + + const addToast = (message: string, type: ToastType, duration: number = 5000) => { + const id = Date.now().toString(); + dispatch({ type: 'ADD_TOAST', payload: { id, message, type, duration } }); + }; + + const removeToast = (id: string) => { + dispatch({ type: 'REMOVE_TOAST', payload: id }); + }; + + // Auto remove toasts after duration + useEffect(() => { + const timers = toasts.map(toast => { + if (toast.duration === 0) return; // 0 means persistent + return setTimeout(() => { + removeToast(toast.id); + }, toast.duration); + }); + + return () => { + timers.forEach(timer => { + if (timer) clearTimeout(timer); + }); + }; + }, [toasts]); + + return ( + + {children} +
+ {toasts.map(toast => ( + + ))} +
+
+ ); +}; + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within a ToastProvider'); + } + return context; +}; \ No newline at end of file diff --git a/src/hooks/useImageGeneration.ts b/src/hooks/useImageGeneration.ts index a39b725..07ed4ea 100644 --- a/src/hooks/useImageGeneration.ts +++ b/src/hooks/useImageGeneration.ts @@ -3,9 +3,11 @@ import { geminiService, GenerationRequest, EditRequest } from '../services/gemin import { useAppStore } from '../store/useAppStore'; import { generateId } from '../utils/imageUtils'; import { Generation, Edit, Asset } from '../types'; +import { useToast } from '../components/ToastContext'; export const useImageGeneration = () => { const { addGeneration, setIsGenerating, setCanvasImage, setCurrentProject, currentProject } = useAppStore(); + const { addToast } = useToast(); const generateMutation = useMutation({ mutationFn: async (request: GenerationRequest) => { @@ -35,15 +37,7 @@ export const useImageGeneration = () => { seed: request.seed, temperature: request.temperature }, - sourceAssets: request.referenceImage ? [{ - id: generateId(), - type: 'original', - url: `data:image/png;base64,${request.referenceImages[0]}`, - mime: 'image/png', - width: 1024, - height: 1024, - checksum: request.referenceImages[0].slice(0, 32) - }] : request.referenceImages ? request.referenceImages.map((img, index) => ({ + sourceAssets: request.referenceImages ? request.referenceImages.map((img, index) => ({ id: generateId(), type: 'original' as const, url: `data:image/png;base64,${img}`, @@ -77,6 +71,7 @@ export const useImageGeneration = () => { }, onError: (error) => { console.error('生成失败:', error); + addToast('图像生成失败,请重试', 'error'); setIsGenerating(false); } }); @@ -99,8 +94,11 @@ export const useImageEditing = () => { selectedGenerationId, currentProject, seed, - temperature + temperature, + uploadedImages } = useAppStore(); + + const { addToast } = useToast(); const editMutation = useMutation({ mutationFn: async (instruction: string) => { @@ -174,12 +172,12 @@ export const useImageEditing = () => { // 绘制带透明度的遮罩叠加 maskedCtx.globalCompositeOperation = 'source-over'; maskedCtx.globalAlpha = 0.4; - maskedCtx.fillStyle = '#A855F7'; + maskedCtx.fillStyle = '#A855F7'; brushStrokes.forEach(stroke => { if (stroke.points.length >= 4) { maskedCtx.lineWidth = stroke.brushSize; - maskedCtx.strokeStyle = '#A855F7'; + maskedCtx.strokeStyle = '#A855F7'; maskedCtx.lineCap = 'round'; maskedCtx.lineJoin = 'round'; maskedCtx.beginPath(); @@ -262,6 +260,7 @@ export const useImageEditing = () => { }, onError: (error) => { console.error('编辑失败:', error); + addToast('图像编辑失败,请重试', 'error'); setIsGenerating(false); } }); diff --git a/src/index.css b/src/index.css index f45de60..7c05186 100644 --- a/src/index.css +++ b/src/index.css @@ -3,6 +3,27 @@ @tailwind components; @tailwind utilities; +/* Toast animations */ +@keyframes slide-in-from-top-full { + 0% { + transform: translateY(-100%); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } +} + +.animate-in { + animation-duration: 300ms; + animation-fill-mode: both; +} + +.slide-in-from-top-full { + animation-name: slide-in-from-top-full; +} + * { box-sizing: border-box; } diff --git a/src/services/geminiService.ts b/src/services/geminiService.ts index 039e5c4..3def898 100644 --- a/src/services/geminiService.ts +++ b/src/services/geminiService.ts @@ -47,17 +47,32 @@ export class GeminiService { contents, }); + // 检查是否有被禁止的内容 + if (response.candidates && response.candidates.length > 0) { + const candidate = response.candidates[0]; + if (candidate.finishReason === 'PROHIBITED_CONTENT') { + throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。'); + } + } + const images: string[] = []; - for (const part of response.candidates[0].content.parts) { - if (part.inlineData) { - images.push(part.inlineData.data); + // 检查响应是否存在以及是否有内容 + if (response.candidates && response.candidates.length > 0 && + response.candidates[0].content && response.candidates[0].content.parts) { + for (const part of response.candidates[0].content.parts) { + if (part.inlineData) { + images.push(part.inlineData.data); + } } } return images; } catch (error) { console.error('生成图像时出错:', error); + if (error instanceof Error && error.message) { + throw error; + } throw new Error('生成图像失败。请重试。'); } } @@ -100,17 +115,32 @@ export class GeminiService { contents, }); + // 检查是否有被禁止的内容 + if (response.candidates && response.candidates.length > 0) { + const candidate = response.candidates[0]; + if (candidate.finishReason === 'PROHIBITED_CONTENT') { + throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。'); + } + } + const images: string[] = []; - for (const part of response.candidates[0].content.parts) { - if (part.inlineData) { - images.push(part.inlineData.data); + // 检查响应是否存在以及是否有内容 + if (response.candidates && response.candidates.length > 0 && + response.candidates[0].content && response.candidates[0].content.parts) { + for (const part of response.candidates[0].content.parts) { + if (part.inlineData) { + images.push(part.inlineData.data); + } } } return images; } catch (error) { console.error('编辑图像时出错:', error); + if (error instanceof Error && error.message) { + throw error; + } throw new Error('编辑图像失败。请重试。'); } } @@ -145,10 +175,21 @@ export class GeminiService { contents: prompt, }); + // 检查是否有被禁止的内容 + if (response.candidates && response.candidates.length > 0) { + const candidate = response.candidates[0]; + if (candidate.finishReason === 'PROHIBITED_CONTENT') { + throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。'); + } + } + const responseText = response.candidates[0].content.parts[0].text; return JSON.parse(responseText); } catch (error) { console.error('分割图像时出错:', error); + if (error instanceof Error && error.message) { + throw error; + } throw new Error('分割图像失败。请重试。'); } }