新增 全局错误toast提示;

This commit is contained in:
2025-09-14 02:54:08 +08:00
parent 9f94e92eaf
commit 46e07cc5ac
7 changed files with 368 additions and 172 deletions

306
README.md
View File

@@ -1,226 +1,226 @@
# 🍌 Nano Banana AI Image Editor # 🍌 Nano Banana AI 图像编辑器
Release Version: (v1.0) 发布版本: (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** ### 🎨 **AI 驱动的创作**
- **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 - **参考图像支持** - 使用最多 2 张参考图像指导生成
- **Advanced Controls** - Fine-tune creativity levels and use custom seeds - **高级控制** - 微调创意水平并使用自定义种子
### ✏️ **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 - **SynthID 水印** - 内置 AI 来源追踪和隐形水印
- **Offline Caching** - IndexedDB storage for offline asset access - **离线缓存** - IndexedDB 存储以实现离线资产访问
- **Type Safety** - Full TypeScript implementation with strict typing - **类型安全** - 完整的 TypeScript 实现和严格类型检查
- **Performance Optimized** - React Query for efficient state management - **性能优化** - React Query 实现高效状态管理
## 🚀 Quick Start ## 🚀 快速开始
### Prerequisites ### 先决条件
- Node.js 18+ - 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 ```bash
git clone <repository-url> git clone <repository-url>
cd nano-banana-image-editor cd nano-banana-image-editor
npm install npm install
``` ```
2. **Configure environment**: 2. **配置环境**:
```bash ```bash
cp .env.example .env 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 ```bash
npm run dev npm run dev
``` ```
4. **Open in browser**: Navigate to `http://localhost:5173` 4. **在浏览器中打开**: 导航到 `http://localhost:5173`
## 🎯 Usage Guide ## 🎯 使用指南
### Creating Images ### 创建图像
1. Select **Generate** mode 1. 选择 **生成** 模式
2. Write a detailed prompt describing your desired image 2. 编写详细提示描述您想要的图像
3. Optionally upload reference images (max 2) 3. 可选上传参考图像(最多 2 张)
4. Adjust creativity settings if needed 4. 如需要调整创意设置
5. Click **Generate** or press `Cmd/Ctrl + Enter` 5. 点击 **生成** 或按 `Cmd/Ctrl + Enter`
### Editing Images ### 编辑图像
1. Switch to **Edit** mode 1. 切换到 **编辑** 模式
2. Upload an image or use a previously generated one 2. 上传图像或使用先前生成的图像
3. Optionally paint a mask to target specific areas 3. 可选绘制遮罩以针对特定区域
4. Describe your desired changes in natural language 4. 用自然语言描述您想要的更改
5. Click **Apply Edit** to see the results 5. 点击 **应用编辑** 查看结果
### Advanced Workflows ### 高级工作流
- Use **Select** mode to paint precise masks for targeted edits - 使用 **选择** 模式绘制精确遮罩以进行目标编辑
- Compare variants in the History panel - 在历史面板中比较变体
- Download high-quality PNG outputs - 下载高质量 PNG 输出
- Use keyboard shortcuts for efficient navigation - 使用键盘快捷键进行高效导航
## ⌨️ Keyboard Shortcuts ## ⌨️ 键盘快捷键
| Shortcut | Action | | 快捷键 | 操作 |
|----------|--------| |----------|--------|
| `Cmd/Ctrl + Enter` | Generate/Apply Edit | | `Cmd/Ctrl + Enter` | 生成/应用编辑 |
| `Shift + R` | Re-roll variants | | `Shift + R` | 重新生成变体 |
| `E` | Switch to Edit mode | | `E` | 切换到编辑模式 |
| `G` | Switch to Generate mode | | `G` | 切换到生成模式 |
| `M` | Switch to Select mode | | `M` | 切换到选择模式 |
| `H` | Toggle history panel | | `H` | 切换历史面板 |
| `P` | Toggle prompt panel | | `P` | 切换提示面板 |
## 🏗️ Architecture ## 🏗️ 架构
### Tech Stack ### 技术栈
- **Frontend**: React 18, TypeScript, Tailwind CSS - **前端**: React 18, TypeScript, Tailwind CSS
- **State Management**: Zustand for app state, React Query for server state - **状态管理**: Zustand 用于应用状态, React Query 用于服务器状态
- **Canvas**: Konva.js for interactive image display and mask overlays - **画布**: Konva.js 用于交互式图像显示和遮罩叠加
- **AI Integration**: Google Generative AI SDK (Gemini 2.5 Flash Image) - **AI 集成**: Google Generative AI SDK (Gemini 2.5 Flash Image)
- **Storage**: IndexedDB for offline asset caching - **存储**: IndexedDB 用于离线资产缓存
- **Build Tool**: Vite for fast development and optimized builds - **构建工具**: Vite 用于快速开发和优化构建
### Project Structure ### 项目结构
``` ```
src/ src/
├── components/ # React components ├── components/ # React 组件
│ ├── ui/ # Reusable UI components (Button, Input, etc.) │ ├── ui/ # 可重用的 UI 组件 (Button, Input, )
│ ├── PromptComposer.tsx # Prompt input and tool selection │ ├── PromptComposer.tsx # 提示输入和工具选择
│ ├── ImageCanvas.tsx # Interactive canvas with Konva │ ├── ImageCanvas.tsx # 使用 Konva 的交互式画布
│ ├── HistoryPanel.tsx # Generation history and variants │ ├── HistoryPanel.tsx # 生成历史和变体
│ ├── Header.tsx # App header and navigation │ ├── Header.tsx # 应用头部和导航
│ └── InfoModal.tsx # About modal with links │ └── InfoModal.tsx # 关于模态框和链接
├── services/ # External service integrations ├── services/ # 外部服务集成
│ ├── geminiService.ts # Gemini API client │ ├── geminiService.ts # Gemini API 客户端
│ ├── cacheService.ts # IndexedDB caching layer │ ├── cacheService.ts # IndexedDB 缓存层
│ └── imageProcessing.ts # Image manipulation utilities │ └── imageProcessing.ts # 图像处理工具
├── store/ # Zustand state management ├── store/ # Zustand 状态管理
│ └── useAppStore.ts # Global application state │ └── useAppStore.ts # 全局应用状态
├── hooks/ # Custom React hooks ├── hooks/ # 自定义 React 钩子
│ ├── useImageGeneration.ts # Generation and editing logic │ ├── useImageGeneration.ts # 生成和编辑逻辑
│ └── useKeyboardShortcuts.ts # Keyboard navigation │ └── useKeyboardShortcuts.ts # 键盘导航
├── utils/ # Utility functions ├── utils/ # 工具函数
│ ├── cn.ts # Class name utility │ ├── cn.ts # 类名工具
│ └── imageUtils.ts # Image processing helpers │ └── imageUtils.ts # 图像处理助手
└── types/ # TypeScript type definitions └── types/ # TypeScript 类型定义
└── index.ts # Core type definitions └── index.ts # 核心类型定义
``` ```
## 🔧 Configuration ## 🔧 配置
### Environment Variables ### 环境变量
```bash ```bash
VITE_GEMINI_API_KEY=your_gemini_api_key_here VITE_GEMINI_API_KEY=your_gemini_api_key_here
``` ```
### Model Configuration ### 模型配置
- **Model**: `gemini-2.5-flash-image-preview` - **模型**: `gemini-2.5-flash-image-preview`
- **Output Format**: 1024×1024 PNG with SynthID watermarks - **输出格式**: 1024×1024 PNG SynthID 水印
- **Input Formats**: PNG, JPEG, WebP - **输入格式**: PNG, JPEG, WebP
- **Temperature Range**: 0-1 (0 = deterministic, 1 = creative) - **温度范围**: 0-1 (0 = 确定性, 1 = 创意)
## 🚀 Deployment ## 🚀 部署
### Development ### 开发
```bash ```bash
npm run dev # Start development server npm run dev # 启动开发服务器
npm run build # Build for production npm run build # 构建生产版本
npm run preview # Preview production build npm run preview # 预览生产构建
npm run lint # Run ESLint npm run lint # 运行 ESLint
``` ```
### Production Considerations ### 生产考虑
- **API Security**: Implement backend proxy for API calls in production - **API 安全**: 在生产环境中为 API 调用实现后端代理
- **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 - **监控**: 添加错误跟踪和分析
## 📄 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 1. **遵循既定模式** - 保持组件在 200 行以内
2. **Maintain type safety** - Use TypeScript strictly with proper definitions 2. **维护类型安全** - 严格使用 TypeScript 和正确定义
3. **Test thoroughly** - Ensure keyboard navigation and accessibility 3. **彻底测试** - 确保键盘导航和可访问性
4. **Document changes** - Update README and add inline comments 4. **记录更改** - 更新 README 并添加内联注释
5. **Respect the license** - All contributions will be under AGPL-3.0 5. **遵守许可证** - 所有贡献都将遵循 AGPL-3.0
## 🔗 Links & Resources ## 🔗 链接和资源
- **Creator**: [Mark Fulton](https://markfulton.com) - **创建者**: [Mark Fulton](https://markfulton.com)
- **AI Training Program**: [Reinventing.AI](https://www.reinventing.ai/) - **AI 培训计划**: [Reinventing.AI](https://www.reinventing.ai/)
- **Community**: [Vibe Coding is Life Skool](https://www.skool.com/vibe-coding-is-life/about?ref=456537abaf37491cbcc6976f3c26af41) - **社区**: [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/) - **Google AI Studio**: [获取您的 API 密钥](https://aistudio.google.com/)
- **Gemini API Docs**: [Official Documentation](https://ai.google.dev/gemini-api/docs) - **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) - **客户端 API 调用** - 目前使用直接 API 调用(在生产环境中实现后端代理)
- **Browser compatibility** - Requires modern browsers with Canvas and WebGL support - **浏览器兼容性** - 需要支持 Canvas WebGL 的现代浏览器
- **Rate limits** - Subject to Google AI Studio rate limits - **速率限制** - 受 Google AI Studio 速率限制约束
- **Image size** - Optimized for 1024×1024 outputs (Gemini model output dimensions may vary) - **图像尺寸** - 针对 1024×1024 输出进行了优化Gemini 模型输出尺寸可能有所不同)
## 🎯 Suggested Updates ## 🎯 建议更新
- [ ] Backend API proxy implementation - [ ] 后端 API 代理实现
- [ ] User authentication and project sharing - [ ] 用户身份验证和项目共享
- [ ] Advanced brush tools and selection methods - [ ] 高级画笔工具和选择方法
- [ ] Plugin system for custom filters - [ ] 自定义过滤器的插件系统
- [ ] Integration with cloud storage providers - [ ] 与云存储提供商集成
--- ---
**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 制作**

View File

@@ -7,6 +7,7 @@ import { ImageCanvas } from './components/ImageCanvas';
import { HistoryPanel } from './components/HistoryPanel'; import { HistoryPanel } from './components/HistoryPanel';
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
import { useAppStore } from './store/useAppStore'; import { useAppStore } from './store/useAppStore';
import { ToastProvider } from './components/ToastContext';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@@ -59,7 +60,9 @@ function AppContent() {
function App() { function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ToastProvider>
<AppContent /> <AppContent />
</ToastProvider>
</QueryClientProvider> </QueryClientProvider>
); );
} }

45
src/components/Toast.tsx Normal file
View File

@@ -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<ToastProps> = ({ 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 (
<div
className={cn(
'flex items-center justify-between p-4 rounded-lg shadow-lg min-w-[300px] max-w-md transition-all duration-300 transform',
getTypeStyles(),
'animate-in slide-in-from-top-full duration-300'
)}
>
<span className="text-sm font-medium">{message}</span>
<button
onClick={() => onClose(id)}
className="ml-4 hover:bg-black/10 rounded-full p-1 transition-colors"
>
<X className="h-4 w-4" />
</button>
</div>
);
};

View File

@@ -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<ToastContextType | undefined>(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 (
<ToastContext.Provider value={{ addToast, removeToast }}>
{children}
<div className="fixed top-4 right-4 z-50 space-y-2">
{toasts.map(toast => (
<Toast
key={toast.id}
id={toast.id}
message={toast.message}
type={toast.type}
onClose={removeToast}
/>
))}
</div>
</ToastContext.Provider>
);
};
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};

View File

@@ -3,9 +3,11 @@ import { geminiService, GenerationRequest, EditRequest } from '../services/gemin
import { useAppStore } from '../store/useAppStore'; import { useAppStore } from '../store/useAppStore';
import { generateId } from '../utils/imageUtils'; import { generateId } from '../utils/imageUtils';
import { Generation, Edit, Asset } from '../types'; import { Generation, Edit, Asset } from '../types';
import { useToast } from '../components/ToastContext';
export const useImageGeneration = () => { export const useImageGeneration = () => {
const { addGeneration, setIsGenerating, setCanvasImage, setCurrentProject, currentProject } = useAppStore(); const { addGeneration, setIsGenerating, setCanvasImage, setCurrentProject, currentProject } = useAppStore();
const { addToast } = useToast();
const generateMutation = useMutation({ const generateMutation = useMutation({
mutationFn: async (request: GenerationRequest) => { mutationFn: async (request: GenerationRequest) => {
@@ -35,15 +37,7 @@ export const useImageGeneration = () => {
seed: request.seed, seed: request.seed,
temperature: request.temperature temperature: request.temperature
}, },
sourceAssets: request.referenceImage ? [{ sourceAssets: request.referenceImages ? request.referenceImages.map((img, index) => ({
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) => ({
id: generateId(), id: generateId(),
type: 'original' as const, type: 'original' as const,
url: `data:image/png;base64,${img}`, url: `data:image/png;base64,${img}`,
@@ -77,6 +71,7 @@ export const useImageGeneration = () => {
}, },
onError: (error) => { onError: (error) => {
console.error('生成失败:', error); console.error('生成失败:', error);
addToast('图像生成失败,请重试', 'error');
setIsGenerating(false); setIsGenerating(false);
} }
}); });
@@ -99,9 +94,12 @@ export const useImageEditing = () => {
selectedGenerationId, selectedGenerationId,
currentProject, currentProject,
seed, seed,
temperature temperature,
uploadedImages
} = useAppStore(); } = useAppStore();
const { addToast } = useToast();
const editMutation = useMutation({ const editMutation = useMutation({
mutationFn: async (instruction: string) => { mutationFn: async (instruction: string) => {
// 如果可用,始终使用画布图像作为主要目标,否则使用第一张上传的图像 // 如果可用,始终使用画布图像作为主要目标,否则使用第一张上传的图像
@@ -262,6 +260,7 @@ export const useImageEditing = () => {
}, },
onError: (error) => { onError: (error) => {
console.error('编辑失败:', error); console.error('编辑失败:', error);
addToast('图像编辑失败,请重试', 'error');
setIsGenerating(false); setIsGenerating(false);
} }
}); });

View File

@@ -3,6 +3,27 @@
@tailwind components; @tailwind components;
@tailwind utilities; @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; box-sizing: border-box;
} }

View File

@@ -47,17 +47,32 @@ export class GeminiService {
contents, contents,
}); });
// 检查是否有被禁止的内容
if (response.candidates && response.candidates.length > 0) {
const candidate = response.candidates[0];
if (candidate.finishReason === 'PROHIBITED_CONTENT') {
throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。');
}
}
const images: string[] = []; const images: string[] = [];
// 检查响应是否存在以及是否有内容
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) { for (const part of response.candidates[0].content.parts) {
if (part.inlineData) { if (part.inlineData) {
images.push(part.inlineData.data); images.push(part.inlineData.data);
} }
} }
}
return images; return images;
} catch (error) { } catch (error) {
console.error('生成图像时出错:', error); console.error('生成图像时出错:', error);
if (error instanceof Error && error.message) {
throw error;
}
throw new Error('生成图像失败。请重试。'); throw new Error('生成图像失败。请重试。');
} }
} }
@@ -100,17 +115,32 @@ export class GeminiService {
contents, contents,
}); });
// 检查是否有被禁止的内容
if (response.candidates && response.candidates.length > 0) {
const candidate = response.candidates[0];
if (candidate.finishReason === 'PROHIBITED_CONTENT') {
throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。');
}
}
const images: string[] = []; const images: string[] = [];
// 检查响应是否存在以及是否有内容
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) { for (const part of response.candidates[0].content.parts) {
if (part.inlineData) { if (part.inlineData) {
images.push(part.inlineData.data); images.push(part.inlineData.data);
} }
} }
}
return images; return images;
} catch (error) { } catch (error) {
console.error('编辑图像时出错:', error); console.error('编辑图像时出错:', error);
if (error instanceof Error && error.message) {
throw error;
}
throw new Error('编辑图像失败。请重试。'); throw new Error('编辑图像失败。请重试。');
} }
} }
@@ -145,10 +175,21 @@ export class GeminiService {
contents: prompt, 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; const responseText = response.candidates[0].content.parts[0].text;
return JSON.parse(responseText); return JSON.parse(responseText);
} catch (error) { } catch (error) {
console.error('分割图像时出错:', error); console.error('分割图像时出错:', error);
if (error instanceof Error && error.message) {
throw error;
}
throw new Error('分割图像失败。请重试。'); throw new Error('分割图像失败。请重试。');
} }
} }