From 9f94e92eafee3b1a9df96be70064a229a6302555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E6=B6=9B?= Date: Sun, 14 Sep 2025 02:05:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 5 - index.html | 24 +-- package.json | 7 +- src/App.tsx | 6 +- src/components/Header.tsx | 12 +- src/components/HistoryPanel.tsx | 120 ++++++------- src/components/ImageCanvas.tsx | 66 +++---- src/components/ImagePreviewModal.tsx | 12 +- src/components/InfoModal.tsx | 45 ++--- src/components/PromptComposer.tsx | 250 ++++++++++++++++----------- src/components/PromptHints.tsx | 18 +- src/components/ui/Button.tsx | 8 +- src/components/ui/Input.tsx | 2 +- src/components/ui/Textarea.tsx | 2 +- src/hooks/useImageGeneration.ts | 44 ++--- src/index.css | 14 +- src/services/geminiService.ts | 42 ++--- src/store/useAppStore.ts | 22 +-- tailwind.config.js | 8 + 19 files changed, 385 insertions(+), 322 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 4f7128f..0000000 --- a/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# Gemini API Configuration -VITE_GEMINI_API_KEY= - -# Note: In production, API calls should go through a backend proxy -# This is for development and demonstration purposes only diff --git a/index.html b/index.html index 1b959b6..90be3b5 100644 --- a/index.html +++ b/index.html @@ -1,9 +1,9 @@ - + - - - + + + - - + + diff --git a/package.json b/package.json index 7264fb6..89bfc62 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,13 @@ { - "name": "vite-react-typescript-starter", + "name": "ano-banana-ai-image-editor", "private": true, "version": "0.0.0", "type": "module", + "description": "一个基于react的 AI 模型实验和原型设计工具,用户可以通过直观的界面与 Google 的大型语言模型(如 Gemini)进行交互", + "repository": { + "type": "git", + "url": "https://git.pandorastudio.cn/yuantao/Nano-Banana-AI-Image-Editor.git" + }, "scripts": { "dev": "vite", "build": "vite build", diff --git a/src/App.tsx b/src/App.tsx index 375bfb4..289ec68 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,7 @@ import { useAppStore } from './store/useAppStore'; const queryClient = new QueryClient({ defaultOptions: { queries: { - staleTime: 5 * 60 * 1000, // 5 minutes + staleTime: 5 * 60 * 1000, // 5分钟 retry: 2, }, }, @@ -22,7 +22,7 @@ function AppContent() { const { showPromptPanel, setShowPromptPanel, showHistory, setShowHistory } = useAppStore(); - // Set mobile defaults on mount + // 在挂载时设置移动设备默认值 React.useEffect(() => { const checkMobile = () => { const isMobile = window.innerWidth < 768; @@ -38,7 +38,7 @@ function AppContent() { }, [setShowPromptPanel, setShowHistory]); return ( -
+
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 44b82b2..5fee007 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -8,18 +8,18 @@ export const Header: React.FC = () => { return ( <> -
+
🍌
-

- Nano Banana AI Image Editor +

+ Nano Banana AI 图像编辑器

-

- NB Editor +

+ NB 编辑器

-
+
1.0
diff --git a/src/components/HistoryPanel.tsx b/src/components/HistoryPanel.tsx index 77a2129..a7c6165 100644 --- a/src/components/HistoryPanel.tsx +++ b/src/components/HistoryPanel.tsx @@ -34,7 +34,7 @@ export const HistoryPanel: React.FC = () => { const generations = currentProject?.generations || []; const edits = currentProject?.edits || []; - // Get current image dimensions + // 获取当前图像尺寸 const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null); React.useEffect(() => { @@ -51,11 +51,11 @@ export const HistoryPanel: React.FC = () => { if (!showHistory) { return ( -
+
- {/* Variants Grid */} + {/* 变体网格 */}
-

Current Variants

+

当前变体

{generations.length === 0 && edits.length === 0 ? (
🖼️
-

No generations yet

+

暂无生成记录

) : (
- {/* Show generations */} + {/* 显示生成记录 */} {generations.slice(-2).map((generation, index) => (
{ <> Generated variant @@ -127,14 +127,14 @@ export const HistoryPanel: React.FC = () => {
)} - {/* Variant Number */} + {/* 变体编号 */}
#{index + 1}
))} - {/* Show edits */} + {/* 显示编辑记录 */} {edits.slice(-2).map((edit, index) => (
{ {edit.outputAssets[0] ? ( Edited variant ) : ( @@ -164,9 +164,9 @@ export const HistoryPanel: React.FC = () => {
)} - {/* Edit Label */} + {/* 编辑标签 */}
- Edit #{index + 1} + 编辑 #{index + 1}
))} @@ -174,28 +174,28 @@ export const HistoryPanel: React.FC = () => { )}
- {/* Current Image Info */} + {/* 当前图像信息 */} {(canvasImage || imageDimensions) && ( -
-

Current Image

+
+

当前图像

{imageDimensions && (
- Dimensions: + 尺寸: {imageDimensions.width} × {imageDimensions.height}
)}
- Mode: + 模式: {selectedTool}
)} - {/* Generation Details */} -
-

Generation Details

+ {/* 生成详情 */} +
+

生成详情

{(() => { const gen = generations.find(g => g.id === selectedGenerationId); const selectedEdit = edits.find(e => e.id === selectedEditId); @@ -205,25 +205,25 @@ export const HistoryPanel: React.FC = () => {
- Prompt: + 提示:

{gen.prompt}

- Model: + 模型: {gen.modelVersion}
{gen.parameters.seed && (
- Seed: + 种子: {gen.parameters.seed}
)}
- {/* Reference Images */} + {/* 参考图像 */} {gen.sourceAssets.length > 0 && (
-
Reference Images
+
参考图像
{gen.sourceAssets.map((asset, index) => ( ))} @@ -260,41 +260,41 @@ export const HistoryPanel: React.FC = () => {
- Edit Instruction: + 编辑指令:

{selectedEdit.instruction}

- Type: - Image Edit + 类型: + 图像编辑
- Created: + 创建时间: {new Date(selectedEdit.timestamp).toLocaleTimeString()}
{selectedEdit.maskAssetId && (
- Mask: - Applied + 遮罩: + 已应用
)}
- {/* Parent Generation Reference */} + {/* 原始生成参考 */} {parentGen && (
-
Original Image
+
原始图像
@@ -336,34 +336,34 @@ export const HistoryPanel: React.FC = () => { } else { return (
-

Select a generation or edit to view details

+

选择一个生成记录或编辑记录以查看详细信息

); } })()}
- {/* Actions */} + {/* 操作 */}
- {/* Image Preview Modal */} + {/* 图像预览模态框 */} setPreviewModal(prev => ({ ...prev, open }))} diff --git a/src/components/ImageCanvas.tsx b/src/components/ImageCanvas.tsx index c42c402..cd3819f 100644 --- a/src/components/ImageCanvas.tsx +++ b/src/components/ImageCanvas.tsx @@ -29,18 +29,18 @@ export const ImageCanvas: React.FC = () => { const [isDrawing, setIsDrawing] = useState(false); const [currentStroke, setCurrentStroke] = useState([]); - // Load image and auto-fit when canvasImage changes + // 加载图像并在 canvasImage 变化时自动适应 useEffect(() => { if (canvasImage) { const img = new window.Image(); img.onload = () => { setImage(img); - // Only auto-fit if this is a new image (no existing zoom/pan state) + // 仅在这是新图像时自动适应(没有现有的缩放/平移状态) if (canvasZoom === 1 && canvasPan.x === 0 && canvasPan.y === 0) { - // Auto-fit image to canvas + // 自动适应图像到画布 const isMobile = window.innerWidth < 768; - const padding = isMobile ? 0.9 : 0.8; // Use more of the screen on mobile + const padding = isMobile ? 0.9 : 0.8; // 在移动设备上使用更多屏幕空间 const scaleX = (stageSize.width * padding) / img.width; const scaleY = (stageSize.height * padding) / img.height; @@ -50,7 +50,7 @@ export const ImageCanvas: React.FC = () => { setCanvasZoom(optimalZoom); - // Center the image + // 居中图像 setCanvasPan({ x: 0, y: 0 }); } }; @@ -60,7 +60,7 @@ export const ImageCanvas: React.FC = () => { } }, [canvasImage, stageSize, setCanvasZoom, setCanvasPan, canvasZoom, canvasPan]); - // Handle stage resize + // 处理舞台大小调整 useEffect(() => { const updateSize = () => { const container = document.getElementById('canvas-container'); @@ -84,18 +84,18 @@ export const ImageCanvas: React.FC = () => { const stage = e.target.getStage(); const pos = stage.getPointerPosition(); - // Use Konva's getRelativePointerPosition for accurate coordinates + // 使用 Konva 的 getRelativePointerPosition 获取准确坐标 const relativePos = stage.getRelativePointerPosition(); - // Calculate image bounds on the stage + // 计算图像在舞台上的边界 const imageX = (stageSize.width / canvasZoom - image.width) / 2; const imageY = (stageSize.height / canvasZoom - image.height) / 2; - // Convert to image-relative coordinates + // 转换为相对于图像的坐标 const relativeX = relativePos.x - imageX; const relativeY = relativePos.y - imageY; - // Check if click is within image bounds + // 检查点击是否在图像边界内 if (relativeX >= 0 && relativeX <= image.width && relativeY >= 0 && relativeY <= image.height) { setCurrentStroke([relativeX, relativeY]); } @@ -107,18 +107,18 @@ export const ImageCanvas: React.FC = () => { const stage = e.target.getStage(); const pos = stage.getPointerPosition(); - // Use Konva's getRelativePointerPosition for accurate coordinates + // 使用 Konva 的 getRelativePointerPosition 获取准确坐标 const relativePos = stage.getRelativePointerPosition(); - // Calculate image bounds on the stage + // 计算图像在舞台上的边界 const imageX = (stageSize.width / canvasZoom - image.width) / 2; const imageY = (stageSize.height / canvasZoom - image.height) / 2; - // Convert to image-relative coordinates + // 转换为相对于图像的坐标 const relativeX = relativePos.x - imageX; const relativeY = relativePos.y - imageY; - // Check if within image bounds + // 检查是否在图像边界内 if (relativeX >= 0 && relativeX <= image.width && relativeY >= 0 && relativeY <= image.height) { setCurrentStroke([...currentStroke, relativeX, relativeY]); } @@ -174,10 +174,10 @@ export const ImageCanvas: React.FC = () => { return (
- {/* Toolbar */} -
+ {/* 工具栏 */} +
- {/* Left side - Zoom controls */} + {/* 左侧 - 缩放控制 */}
- {/* Right side - Tools and actions */} + {/* 右侧 - 工具和操作 */}
{selectedTool === 'mask' && ( <>
- Brush: + 画笔: { className={cn(showMasks && 'bg-yellow-400/10 border-yellow-400/50')} > {showMasks ? : } - Masks + 遮罩 {canvasImage && ( )}
- {/* Canvas Area */} + {/* 画布区域 */}
{!image && !isGenerating && (
🍌

- Welcome to Nano Banana Framework + 欢迎使用 Nano Banana 框架

{selectedTool === 'generate' - ? 'Start by describing what you want to create in the prompt box' - : 'Upload an image to begin editing' + ? '首先在提示框中描述您想要创建的内容' + : '上传图像开始编辑' }

@@ -266,7 +266,7 @@ export const ImageCanvas: React.FC = () => {
-

Creating your image...

+

正在创建您的图像...

)} @@ -302,7 +302,7 @@ export const ImageCanvas: React.FC = () => { /> )} - {/* Brush Strokes */} + {/* 画笔描边 */} {showMasks && brushStrokes.map((stroke) => ( { /> ))} - {/* Current stroke being drawn */} + {/* 正在绘制的当前描边 */} {isDrawing && currentStroke.length > 2 && ( {
- {/* Status Bar */} -
+ {/* 状态栏 */} +
{brushStrokes.length > 0 && ( - {brushStrokes.length} brush stroke{brushStrokes.length !== 1 ? 's' : ''} + {brushStrokes.length} 个画笔描边{brushStrokes.length !== 1 ? 's' : ''} )}
@@ -361,7 +361,7 @@ export const ImageCanvas: React.FC = () => { - Powered by Gemini 2.5 Flash Image + 由 Gemini 2.5 Flash Image 提供支持
diff --git a/src/components/ImagePreviewModal.tsx b/src/components/ImagePreviewModal.tsx index 28dcdd7..5380dda 100644 --- a/src/components/ImagePreviewModal.tsx +++ b/src/components/ImagePreviewModal.tsx @@ -21,10 +21,10 @@ export const ImagePreviewModal: React.FC = ({ return ( - - + +
- + {title} @@ -36,14 +36,14 @@ export const ImagePreviewModal: React.FC = ({
{description && ( -

{description}

+

{description}

)} -
+
{title}
diff --git a/src/components/InfoModal.tsx b/src/components/InfoModal.tsx index cc0cd3f..2bda3e7 100644 --- a/src/components/InfoModal.tsx +++ b/src/components/InfoModal.tsx @@ -13,10 +13,10 @@ export const InfoModal: React.FC = ({ open, onOpenChange }) => { - +
- - About Nano Banana AI Image Editor + + 关于 Nano Banana AI 图像编辑器
-
+

- Developed by{' '} + 由{' '} Mark Fulton + 开发

-
+
- -

- Learn to Build AI Apps & More Solutions + +

+ 学习构建AI应用和其他解决方案

-

- Learn to vibe code apps like this one and master AI automation, build intelligent agents, and create cutting-edge solutions that drive real business results. +

+ 学习编写像这样的应用程序,掌握AI自动化,构建智能代理,并创建推动实际业务成果的前沿解决方案。

- Join the AI Accelerator Program + 加入AI加速器计划
-
+
- -

- Get a Copy of This App + +

+ 获取此应用程序的副本

-

- Get a copy of this app by joining the Vibe Coding is Life Skool community. Live build sessions, app projects, resources and more in the best vibe coding community on the web. +

+ 通过加入Vibe Coding is Life Skool社区获取此应用程序的副本。现场构建会话、应用程序项目、资源等,这是网络上最好的氛围编码社区。

- Join Vibe Coding is Life Community + 加入Vibe Coding is Life社区
diff --git a/src/components/PromptComposer.tsx b/src/components/PromptComposer.tsx index 7bcee62..afc2bd7 100644 --- a/src/components/PromptComposer.tsx +++ b/src/components/PromptComposer.tsx @@ -39,6 +39,7 @@ export const PromptComposer: React.FC = () => { const [showAdvanced, setShowAdvanced] = useState(false); const [showClearConfirm, setShowClearConfirm] = useState(false); const [showHintsModal, setShowHintsModal] = useState(false); + const [isDragOver, setIsDragOver] = useState(false); const fileInputRef = useRef(null); const handleGenerate = () => { @@ -60,39 +61,64 @@ export const PromptComposer: React.FC = () => { } }; - const handleFileUpload = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; + const handleFileUpload = async (file: File) => { if (file && file.type.startsWith('image/')) { try { const base64 = await blobToBase64(file); const dataUrl = `data:${file.type};base64,${base64}`; if (selectedTool === 'generate') { - // Add to reference images (max 2) + // 添加到参考图像(最多2张) if (uploadedImages.length < 2) { addUploadedImage(dataUrl); } } else if (selectedTool === 'edit') { - // For edit mode, add to separate edit reference images (max 2) + // 编辑模式下,添加到单独的编辑参考图像(最多2张) if (editReferenceImages.length < 2) { addEditReferenceImage(dataUrl); } - // Set as canvas image if none exists + // 如果没有画布图像,则设置为画布图像 if (!canvasImage) { setCanvasImage(dataUrl); } } else if (selectedTool === 'mask') { - // For mask mode, set as canvas image immediately + // 遮罩模式下,立即设置为画布图像 clearUploadedImages(); addUploadedImage(dataUrl); setCanvasImage(dataUrl); } } catch (error) { - console.error('Failed to upload image:', error); + console.error('上传图像失败:', error); } } }; + const handleFileInputChange = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + await handleFileUpload(file); + } + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + setIsDragOver(true); + }; + + const handleDragLeave = () => { + setIsDragOver(false); + }; + + const handleDrop = async (event: React.DragEvent) => { + event.preventDefault(); + setIsDragOver(false); + + const file = event.dataTransfer.files?.[0]; + if (file) { + await handleFileUpload(file); + } + }; + const handleClearSession = () => { setCurrentPrompt(''); clearUploadedImages(); @@ -105,18 +131,18 @@ export const PromptComposer: React.FC = () => { }; const tools = [ - { id: 'generate', icon: Wand2, label: 'Generate', description: 'Create from text' }, - { id: 'edit', icon: Edit3, label: 'Edit', description: 'Modify existing' }, - { id: 'mask', icon: MousePointer, label: 'Select', description: 'Click to select' }, + { id: 'generate', icon: Wand2, label: '生成', description: '从文本创建' }, + { id: 'edit', icon: Edit3, label: '编辑', description: '修改现有图像' }, + { id: 'mask', icon: MousePointer, label: '选择', description: '点击选择' }, ] as const; if (!showPromptPanel) { return ( -
+
@@ -163,7 +189,7 @@ export const PromptComposer: React.FC = () => { 'flex flex-col items-center p-3 rounded-lg border transition-all duration-200', selectedTool === tool.id ? 'bg-yellow-400/10 border-yellow-400/50 text-yellow-400' - : 'bg-gray-900 border-gray-700 text-gray-400 hover:bg-gray-800 hover:text-gray-300' + : 'bg-white border-yellow-400/50 text-gray-400 hover:bg-yellow-400 hover:text-white' )} > @@ -173,42 +199,69 @@ export const PromptComposer: React.FC = () => {
- {/* File Upload */} + {/* 文件上传 */}
-
- - {selectedTool === 'mask' && ( -

Edit an image with masks

- )} - {selectedTool === 'generate' && ( -

Optional, up to 2 images

- )} - {selectedTool === 'edit' && ( -

- {canvasImage ? 'Optional style references, up to 2 images' : 'Upload image to edit, up to 2 images'} -

- )} - - + + {selectedTool === 'mask' && ( +

使用遮罩编辑图像

+ )} + {selectedTool === 'generate' && ( +

可选,最多2张图像

+ )} + {selectedTool === 'edit' && ( +

+ {canvasImage ? '可选样式参考,最多2张图像' : '上传要编辑的图像,最多2张图像'} +

+ )} + + + +
+ +
+

+ {isDragOver ? "释放文件以上传" : "拖拽图像到此处或点击上传"} +

+

+ 支持 JPG, PNG, GIF 格式 +

+
+ +
+
{/* Show uploaded images preview */} {((selectedTool === 'generate' && uploadedImages.length > 0) || @@ -218,45 +271,44 @@ export const PromptComposer: React.FC = () => {
{`Reference -
- Ref {index + 1} +
+ 参考 {index + 1}
))}
)} -
- {/* Prompt Input */} + {/* 提示输入 */}
-