初始化提交

This commit is contained in:
2025-09-14 02:05:42 +08:00
parent 1a3730454e
commit 9f94e92eaf
19 changed files with 385 additions and 322 deletions

View File

@@ -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

View File

@@ -1,9 +1,9 @@
<!doctype html> <!doctype html>
<html lang="en" class="dark"> <html lang="zh" class="dark">
<head> <head>
<!-- <!--
Nano Banana AI Image Editor ©2025 Mark Fulton Nano Banana AI 图像编辑器 ©2025 Mark Fulton
Join the Vibe Coding is Life Skool to get your copy: 加入 Vibe Coding is Life Skool 获取您的副本:
👉🏼 https://www.skool.com/vibe-coding-is-life 👉🏼 https://www.skool.com/vibe-coding-is-life
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠖⠛⠉⠉⠛⠛⠹⢷⡦⣄ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠖⠛⠉⠉⠛⠛⠹⢷⡦⣄
@@ -49,24 +49,24 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍌</text></svg>" /> <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍌</text></svg>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nano Banana AI Image Editor - AI Image Generator & Editor</title> <title>Nano Banana AI 图像编辑器 - AI 图像生成器和编辑器</title>
<meta name="description" content="Professional AI image generation and conversational editing powered by Gemini 2.5 Flash Image. Create, edit, and enhance images with natural language prompts." /> <meta name="description" content="由 Gemini 2.5 Flash Image 提供支持的专业 AI 图像生成和对话式编辑。使用自然语言提示创建、编辑和增强图像。" />
<meta name="keywords" content="AI image generation, image editing, Gemini AI, text to image, image enhancement, artificial intelligence" /> <meta name="keywords" content="AI图像生成, 图像编辑, Gemini AI, 文本到图像, 图像增强, 人工智能" />
<meta name="author" content="Mark Fulton" /> <meta name="author" content="Mark Fulton" />
<meta name="copyright" content="©2025 Mark Fulton (https://markfulton.com)" /> <meta name="copyright" content="©2025 Mark Fulton (https://markfulton.com)" />
<meta name="license" content="GNU Affero General Public License v3.0" /> <meta name="license" content="GNU Affero General Public License v3.0" />
<meta name="application-name" content="Nano Banana Image Editor" /> <meta name="application-name" content="Nano Banana 图像编辑器" />
<!-- Open Graph / Facebook --> <!-- Open Graph / Facebook -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="Nano Banana Image Editor - AI Image Generation & Editing" /> <meta property="og:title" content="Nano Banana 图像编辑器 - AI 图像生成和编辑" />
<meta property="og:description" content="Professional AI image generation and conversational editing powered by Gemini 2.5 Flash Image" /> <meta property="og:description" content=" Gemini 2.5 Flash Image 提供支持的专业 AI 图像生成和对话式编辑" />
<meta property="og:site_name" content="Nano Banana Image Editor" /> <meta property="og:site_name" content="Nano Banana 图像编辑器" />
<!-- Twitter --> <!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Nano Banana Image Editor - AI Image Generation & Editing" /> <meta name="twitter:title" content="Nano Banana 图像编辑器 - AI 图像生成和编辑" />
<meta name="twitter:description" content="Professional AI image generation and conversational editing powered by Gemini 2.5 Flash Image" /> <meta name="twitter:description" content=" Gemini 2.5 Flash Image 提供支持的专业 AI 图像生成和对话式编辑" />
<!-- Additional Meta --> <!-- Additional Meta -->
<meta name="theme-color" content="#FDE047" /> <meta name="theme-color" content="#FDE047" />

View File

@@ -1,8 +1,13 @@
{ {
"name": "vite-react-typescript-starter", "name": "ano-banana-ai-image-editor",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"description": "一个基于react的 AI 模型实验和原型设计工具,用户可以通过直观的界面与 Google 的大型语言模型(如 Gemini进行交互",
"repository": {
"type": "git",
"url": "https://git.pandorastudio.cn/yuantao/Nano-Banana-AI-Image-Editor.git"
},
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",

View File

@@ -11,7 +11,7 @@ import { useAppStore } from './store/useAppStore';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
staleTime: 5 * 60 * 1000, // 5 minutes staleTime: 5 * 60 * 1000, // 5分钟
retry: 2, retry: 2,
}, },
}, },
@@ -22,7 +22,7 @@ function AppContent() {
const { showPromptPanel, setShowPromptPanel, showHistory, setShowHistory } = useAppStore(); const { showPromptPanel, setShowPromptPanel, showHistory, setShowHistory } = useAppStore();
// Set mobile defaults on mount // 在挂载时设置移动设备默认值
React.useEffect(() => { React.useEffect(() => {
const checkMobile = () => { const checkMobile = () => {
const isMobile = window.innerWidth < 768; const isMobile = window.innerWidth < 768;
@@ -38,7 +38,7 @@ function AppContent() {
}, [setShowPromptPanel, setShowHistory]); }, [setShowPromptPanel, setShowHistory]);
return ( return (
<div className="h-screen bg-gray-900 text-gray-100 flex flex-col font-sans"> <div className="h-screen bg-white text-gray-900 flex flex-col font-sans">
<Header /> <Header />
<div className="flex-1 flex overflow-hidden"> <div className="flex-1 flex overflow-hidden">

View File

@@ -8,18 +8,18 @@ export const Header: React.FC = () => {
return ( return (
<> <>
<header className="h-16 bg-gray-950 border-b border-gray-800 flex items-center justify-between px-6"> <header className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="text-2xl">🍌</div> <div className="text-2xl">🍌</div>
<h1 className="text-xl font-semibold text-gray-100 hidden md:block"> <h1 className="text-xl font-semibold text-gray-900 hidden md:block">
Nano Banana AI Image Editor Nano Banana AI
</h1> </h1>
<h1 className="text-xl font-semibold text-gray-100 md:hidden"> <h1 className="text-xl font-semibold text-gray-900 md:hidden">
NB Editor NB
</h1> </h1>
</div> </div>
<div className="text-xs text-gray-500 bg-gray-800 px-2 py-1 rounded"> <div className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
1.0 1.0
</div> </div>
</div> </div>

View File

@@ -34,7 +34,7 @@ export const HistoryPanel: React.FC = () => {
const generations = currentProject?.generations || []; const generations = currentProject?.generations || [];
const edits = currentProject?.edits || []; const edits = currentProject?.edits || [];
// Get current image dimensions // 获取当前图像尺寸
const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null); const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null);
React.useEffect(() => { React.useEffect(() => {
@@ -51,11 +51,11 @@ export const HistoryPanel: React.FC = () => {
if (!showHistory) { if (!showHistory) {
return ( return (
<div className="w-8 bg-gray-950 border-l border-gray-800 flex flex-col items-center justify-center"> <div className="w-8 bg-white border-l border-gray-200 flex flex-col items-center justify-center">
<button <button
onClick={() => setShowHistory(true)} onClick={() => setShowHistory(true)}
className="w-6 h-16 bg-gray-800 hover:bg-gray-700 rounded-l-lg border border-r-0 border-gray-700 flex items-center justify-center transition-colors group" className="w-6 h-16 bg-gray-100 hover:bg-gray-200 rounded-l-lg border border-r-0 border-gray-300 flex items-center justify-center transition-colors group"
title="Show History Panel" title="显示历史面板"
> >
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full"></div> <div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full"></div>
@@ -68,35 +68,35 @@ export const HistoryPanel: React.FC = () => {
} }
return ( return (
<div className="w-80 bg-gray-950 border-l border-gray-800 p-6 flex flex-col h-full"> <div className="w-80 bg-white border-l border-gray-200 p-6 flex flex-col h-full">
{/* Header */} {/* 头部 */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<History className="h-5 w-5 text-gray-400" /> <History className="h-5 w-5 text-gray-400" />
<h3 className="text-sm font-medium text-gray-300">History & Variants</h3> <h3 className="text-sm font-medium text-gray-300"></h3>
</div> </div>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => setShowHistory(!showHistory)} onClick={() => setShowHistory(!showHistory)}
className="h-6 w-6" className="h-6 w-6"
title="Hide History Panel" title="隐藏历史面板"
> >
× ×
</Button> </Button>
</div> </div>
{/* Variants Grid */} {/* 变体网格 */}
<div className="mb-6 flex-shrink-0"> <div className="mb-6 flex-shrink-0">
<h4 className="text-xs font-medium text-gray-400 mb-3">Current Variants</h4> <h4 className="text-xs font-medium text-gray-400 mb-3"></h4>
{generations.length === 0 && edits.length === 0 ? ( {generations.length === 0 && edits.length === 0 ? (
<div className="text-center py-8"> <div className="text-center py-8">
<div className="text-4xl mb-2">🖼</div> <div className="text-4xl mb-2">🖼</div>
<p className="text-sm text-gray-500">No generations yet</p> <p className="text-sm text-gray-500"></p>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{/* Show generations */} {/* 显示生成记录 */}
{generations.slice(-2).map((generation, index) => ( {generations.slice(-2).map((generation, index) => (
<div <div
key={generation.id} key={generation.id}
@@ -117,7 +117,7 @@ export const HistoryPanel: React.FC = () => {
<> <>
<img <img
src={generation.outputAssets[0].url} src={generation.outputAssets[0].url}
alt="Generated variant" alt="生成的变体"
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
</> </>
@@ -127,14 +127,14 @@ export const HistoryPanel: React.FC = () => {
</div> </div>
)} )}
{/* Variant Number */} {/* 变体编号 */}
<div className="absolute top-2 left-2 bg-gray-900/80 text-xs px-2 py-1 rounded"> <div className="absolute top-2 left-2 bg-gray-900/80 text-xs px-2 py-1 rounded">
#{index + 1} #{index + 1}
</div> </div>
</div> </div>
))} ))}
{/* Show edits */} {/* 显示编辑记录 */}
{edits.slice(-2).map((edit, index) => ( {edits.slice(-2).map((edit, index) => (
<div <div
key={edit.id} key={edit.id}
@@ -155,7 +155,7 @@ export const HistoryPanel: React.FC = () => {
{edit.outputAssets[0] ? ( {edit.outputAssets[0] ? (
<img <img
src={edit.outputAssets[0].url} src={edit.outputAssets[0].url}
alt="Edited variant" alt="编辑的变体"
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
@@ -164,9 +164,9 @@ export const HistoryPanel: React.FC = () => {
</div> </div>
)} )}
{/* Edit Label */} {/* 编辑标签 */}
<div className="absolute top-2 left-2 bg-purple-900/80 text-xs px-2 py-1 rounded"> <div className="absolute top-2 left-2 bg-purple-900/80 text-xs px-2 py-1 rounded">
Edit #{index + 1} #{index + 1}
</div> </div>
</div> </div>
))} ))}
@@ -174,28 +174,28 @@ export const HistoryPanel: React.FC = () => {
)} )}
</div> </div>
{/* Current Image Info */} {/* 当前图像信息 */}
{(canvasImage || imageDimensions) && ( {(canvasImage || imageDimensions) && (
<div className="mb-4 p-3 bg-gray-900 rounded-lg border border-gray-700"> <div className="mb-4 p-3 bg-gray-100 rounded-lg border border-gray-300">
<h4 className="text-xs font-medium text-gray-400 mb-2">Current Image</h4> <h4 className="text-xs font-medium text-gray-400 mb-2"></h4>
<div className="space-y-1 text-xs text-gray-500"> <div className="space-y-1 text-xs text-gray-500">
{imageDimensions && ( {imageDimensions && (
<div className="flex justify-between"> <div className="flex justify-between">
<span>Dimensions:</span> <span>:</span>
<span className="text-gray-300">{imageDimensions.width} × {imageDimensions.height}</span> <span className="text-gray-300">{imageDimensions.width} × {imageDimensions.height}</span>
</div> </div>
)} )}
<div className="flex justify-between"> <div className="flex justify-between">
<span>Mode:</span> <span>:</span>
<span className="text-gray-300 capitalize">{selectedTool}</span> <span className="text-gray-300 capitalize">{selectedTool}</span>
</div> </div>
</div> </div>
</div> </div>
)} )}
{/* Generation Details */} {/* 生成详情 */}
<div className="mb-6 p-4 bg-gray-900 rounded-lg border border-gray-700 flex-1 overflow-y-auto min-h-0"> <div className="mb-6 p-4 bg-gray-100 rounded-lg border border-gray-300 flex-1 overflow-y-auto min-h-0">
<h4 className="text-xs font-medium text-gray-400 mb-2">Generation Details</h4> <h4 className="text-xs font-medium text-gray-400 mb-2"></h4>
{(() => { {(() => {
const gen = generations.find(g => g.id === selectedGenerationId); const gen = generations.find(g => g.id === selectedGenerationId);
const selectedEdit = edits.find(e => e.id === selectedEditId); const selectedEdit = edits.find(e => e.id === selectedEditId);
@@ -205,25 +205,25 @@ export const HistoryPanel: React.FC = () => {
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2 text-xs text-gray-500"> <div className="space-y-2 text-xs text-gray-500">
<div> <div>
<span className="text-gray-400">Prompt:</span> <span className="text-gray-400">:</span>
<p className="text-gray-300 mt-1">{gen.prompt}</p> <p className="text-gray-300 mt-1">{gen.prompt}</p>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>Model:</span> <span>:</span>
<span>{gen.modelVersion}</span> <span>{gen.modelVersion}</span>
</div> </div>
{gen.parameters.seed && ( {gen.parameters.seed && (
<div className="flex justify-between"> <div className="flex justify-between">
<span>Seed:</span> <span>:</span>
<span>{gen.parameters.seed}</span> <span>{gen.parameters.seed}</span>
</div> </div>
)} )}
</div> </div>
{/* Reference Images */} {/* 参考图像 */}
{gen.sourceAssets.length > 0 && ( {gen.sourceAssets.length > 0 && (
<div> <div>
<h5 className="text-xs font-medium text-gray-400 mb-2">Reference Images</h5> <h5 className="text-xs font-medium text-gray-400 mb-2"></h5>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{gen.sourceAssets.map((asset, index) => ( {gen.sourceAssets.map((asset, index) => (
<button <button
@@ -231,21 +231,21 @@ export const HistoryPanel: React.FC = () => {
onClick={() => setPreviewModal({ onClick={() => setPreviewModal({
open: true, open: true,
imageUrl: asset.url, imageUrl: asset.url,
title: `Reference Image ${index + 1}`, title: `参考图像 ${index + 1}`,
description: 'This reference image was used to guide the generation' description: '此参考图像用于指导生成'
})} })}
className="relative aspect-square rounded border border-gray-700 hover:border-gray-600 transition-colors overflow-hidden group" className="relative aspect-square rounded border border-gray-700 hover:border-gray-600 transition-colors overflow-hidden group"
> >
<img <img
src={asset.url} src={asset.url}
alt={`Reference ${index + 1}`} alt={`参考 ${index + 1}`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center"> <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center">
<ImageIcon className="h-4 w-4 text-white opacity-0 group-hover:opacity-100 transition-opacity" /> <ImageIcon className="h-4 w-4 text-white opacity-0 group-hover:opacity-100 transition-opacity" />
</div> </div>
<div className="absolute bottom-1 left-1 bg-gray-900/80 text-xs px-1 py-0.5 rounded text-gray-300"> <div className="absolute bottom-1 left-1 bg-gray-900/80 text-xs px-1 py-0.5 rounded text-gray-300">
Ref {index + 1} {index + 1}
</div> </div>
</button> </button>
))} ))}
@@ -260,41 +260,41 @@ export const HistoryPanel: React.FC = () => {
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2 text-xs text-gray-500"> <div className="space-y-2 text-xs text-gray-500">
<div> <div>
<span className="text-gray-400">Edit Instruction:</span> <span className="text-gray-400">:</span>
<p className="text-gray-300 mt-1">{selectedEdit.instruction}</p> <p className="text-gray-300 mt-1">{selectedEdit.instruction}</p>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>Type:</span> <span>:</span>
<span>Image Edit</span> <span></span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>Created:</span> <span>:</span>
<span>{new Date(selectedEdit.timestamp).toLocaleTimeString()}</span> <span>{new Date(selectedEdit.timestamp).toLocaleTimeString()}</span>
</div> </div>
{selectedEdit.maskAssetId && ( {selectedEdit.maskAssetId && (
<div className="flex justify-between"> <div className="flex justify-between">
<span>Mask:</span> <span>:</span>
<span className="text-purple-400">Applied</span> <span className="text-purple-400"></span>
</div> </div>
)} )}
</div> </div>
{/* Parent Generation Reference */} {/* 原始生成参考 */}
{parentGen && ( {parentGen && (
<div> <div>
<h5 className="text-xs font-medium text-gray-400 mb-2">Original Image</h5> <h5 className="text-xs font-medium text-gray-400 mb-2"></h5>
<button <button
onClick={() => setPreviewModal({ onClick={() => setPreviewModal({
open: true, open: true,
imageUrl: parentGen.outputAssets[0]?.url || '', imageUrl: parentGen.outputAssets[0]?.url || '',
title: 'Original Image', title: '原始图像',
description: 'The base image that was edited' description: '被编辑的基础图像'
})} })}
className="relative aspect-square w-16 rounded border border-gray-700 hover:border-gray-600 transition-colors overflow-hidden group" className="relative aspect-square w-16 rounded border border-gray-700 hover:border-gray-600 transition-colors overflow-hidden group"
> >
<img <img
src={parentGen.outputAssets[0]?.url} src={parentGen.outputAssets[0]?.url}
alt="Original" alt="原始"
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center"> <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center">
@@ -304,29 +304,29 @@ export const HistoryPanel: React.FC = () => {
</div> </div>
)} )}
{/* Mask Visualization */} {/* 遮罩可视化 */}
{selectedEdit.maskReferenceAsset && ( {selectedEdit.maskReferenceAsset && (
<div> <div>
<h5 className="text-xs font-medium text-gray-400 mb-2">Masked Reference</h5> <h5 className="text-xs font-medium text-gray-400 mb-2"></h5>
<button <button
onClick={() => setPreviewModal({ onClick={() => setPreviewModal({
open: true, open: true,
imageUrl: selectedEdit.maskReferenceAsset!.url, imageUrl: selectedEdit.maskReferenceAsset!.url,
title: 'Masked Reference Image', title: '遮罩参考图像',
description: 'This image with mask overlay was sent to the AI model to guide the edit' description: '带有遮罩叠加的图像被发送到AI模型以指导编辑'
})} })}
className="relative aspect-square w-16 rounded border border-gray-700 hover:border-gray-600 transition-colors overflow-hidden group" className="relative aspect-square w-16 rounded border border-gray-700 hover:border-gray-600 transition-colors overflow-hidden group"
> >
<img <img
src={selectedEdit.maskReferenceAsset.url} src={selectedEdit.maskReferenceAsset.url}
alt="Masked reference" alt="遮罩参考"
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center"> <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center">
<ImageIcon className="h-3 w-3 text-white opacity-0 group-hover:opacity-100 transition-opacity" /> <ImageIcon className="h-3 w-3 text-white opacity-0 group-hover:opacity-100 transition-opacity" />
</div> </div>
<div className="absolute bottom-1 left-1 bg-purple-900/80 text-xs px-1 py-0.5 rounded text-purple-300"> <div className="absolute bottom-1 left-1 bg-purple-900/80 text-xs px-1 py-0.5 rounded text-purple-300">
Mask
</div> </div>
</button> </button>
</div> </div>
@@ -336,34 +336,34 @@ export const HistoryPanel: React.FC = () => {
} else { } else {
return ( return (
<div className="space-y-2 text-xs text-gray-500"> <div className="space-y-2 text-xs text-gray-500">
<p className="text-gray-400">Select a generation or edit to view details</p> <p className="text-gray-400"></p>
</div> </div>
); );
} }
})()} })()}
</div> </div>
{/* Actions */} {/* 操作 */}
<div className="space-y-3 flex-shrink-0"> <div className="space-y-3 flex-shrink-0">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="w-full" className="w-full"
onClick={() => { onClick={() => {
// Find the currently displayed image (either generation or edit) // 查找当前显示的图像(生成记录或编辑记录)
let imageUrl: string | null = null; let imageUrl: string | null = null;
if (selectedGenerationId) { if (selectedGenerationId) {
const gen = generations.find(g => g.id === selectedGenerationId); const gen = generations.find(g => g.id === selectedGenerationId);
imageUrl = gen?.outputAssets[0]?.url || null; imageUrl = gen?.outputAssets[0]?.url || null;
} else { } else {
// If no generation selected, try to get the current canvas image // 如果没有选择生成记录,尝试获取当前画布图像
const { canvasImage } = useAppStore.getState(); const { canvasImage } = useAppStore.getState();
imageUrl = canvasImage; imageUrl = canvasImage;
} }
if (imageUrl) { if (imageUrl) {
// Handle both data URLs and regular URLs // 处理数据URL和常规URL
if (imageUrl.startsWith('data:')) { if (imageUrl.startsWith('data:')) {
const link = document.createElement('a'); const link = document.createElement('a');
link.href = imageUrl; link.href = imageUrl;
@@ -372,7 +372,7 @@ export const HistoryPanel: React.FC = () => {
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
} else { } else {
// For external URLs, we need to fetch and convert to blob // 对于外部URL我们需要获取并转换为blob
fetch(imageUrl) fetch(imageUrl)
.then(response => response.blob()) .then(response => response.blob())
.then(blob => { .then(blob => {
@@ -391,11 +391,11 @@ export const HistoryPanel: React.FC = () => {
disabled={!selectedGenerationId && !useAppStore.getState().canvasImage} disabled={!selectedGenerationId && !useAppStore.getState().canvasImage}
> >
<Download className="h-4 w-4 mr-2" /> <Download className="h-4 w-4 mr-2" />
Download
</Button> </Button>
</div> </div>
{/* Image Preview Modal */} {/* 图像预览模态框 */}
<ImagePreviewModal <ImagePreviewModal
open={previewModal.open} open={previewModal.open}
onOpenChange={(open) => setPreviewModal(prev => ({ ...prev, open }))} onOpenChange={(open) => setPreviewModal(prev => ({ ...prev, open }))}

View File

@@ -29,18 +29,18 @@ export const ImageCanvas: React.FC = () => {
const [isDrawing, setIsDrawing] = useState(false); const [isDrawing, setIsDrawing] = useState(false);
const [currentStroke, setCurrentStroke] = useState<number[]>([]); const [currentStroke, setCurrentStroke] = useState<number[]>([]);
// Load image and auto-fit when canvasImage changes // 加载图像并在 canvasImage 变化时自动适应
useEffect(() => { useEffect(() => {
if (canvasImage) { if (canvasImage) {
const img = new window.Image(); const img = new window.Image();
img.onload = () => { img.onload = () => {
setImage(img); 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) { if (canvasZoom === 1 && canvasPan.x === 0 && canvasPan.y === 0) {
// Auto-fit image to canvas // 自动适应图像到画布
const isMobile = window.innerWidth < 768; 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 scaleX = (stageSize.width * padding) / img.width;
const scaleY = (stageSize.height * padding) / img.height; const scaleY = (stageSize.height * padding) / img.height;
@@ -50,7 +50,7 @@ export const ImageCanvas: React.FC = () => {
setCanvasZoom(optimalZoom); setCanvasZoom(optimalZoom);
// Center the image // 居中图像
setCanvasPan({ x: 0, y: 0 }); setCanvasPan({ x: 0, y: 0 });
} }
}; };
@@ -60,7 +60,7 @@ export const ImageCanvas: React.FC = () => {
} }
}, [canvasImage, stageSize, setCanvasZoom, setCanvasPan, canvasZoom, canvasPan]); }, [canvasImage, stageSize, setCanvasZoom, setCanvasPan, canvasZoom, canvasPan]);
// Handle stage resize // 处理舞台大小调整
useEffect(() => { useEffect(() => {
const updateSize = () => { const updateSize = () => {
const container = document.getElementById('canvas-container'); const container = document.getElementById('canvas-container');
@@ -84,18 +84,18 @@ export const ImageCanvas: React.FC = () => {
const stage = e.target.getStage(); const stage = e.target.getStage();
const pos = stage.getPointerPosition(); const pos = stage.getPointerPosition();
// Use Konva's getRelativePointerPosition for accurate coordinates // 使用 Konva getRelativePointerPosition 获取准确坐标
const relativePos = stage.getRelativePointerPosition(); const relativePos = stage.getRelativePointerPosition();
// Calculate image bounds on the stage // 计算图像在舞台上的边界
const imageX = (stageSize.width / canvasZoom - image.width) / 2; const imageX = (stageSize.width / canvasZoom - image.width) / 2;
const imageY = (stageSize.height / canvasZoom - image.height) / 2; const imageY = (stageSize.height / canvasZoom - image.height) / 2;
// Convert to image-relative coordinates // 转换为相对于图像的坐标
const relativeX = relativePos.x - imageX; const relativeX = relativePos.x - imageX;
const relativeY = relativePos.y - imageY; const relativeY = relativePos.y - imageY;
// Check if click is within image bounds // 检查点击是否在图像边界内
if (relativeX >= 0 && relativeX <= image.width && relativeY >= 0 && relativeY <= image.height) { if (relativeX >= 0 && relativeX <= image.width && relativeY >= 0 && relativeY <= image.height) {
setCurrentStroke([relativeX, relativeY]); setCurrentStroke([relativeX, relativeY]);
} }
@@ -107,18 +107,18 @@ export const ImageCanvas: React.FC = () => {
const stage = e.target.getStage(); const stage = e.target.getStage();
const pos = stage.getPointerPosition(); const pos = stage.getPointerPosition();
// Use Konva's getRelativePointerPosition for accurate coordinates // 使用 Konva getRelativePointerPosition 获取准确坐标
const relativePos = stage.getRelativePointerPosition(); const relativePos = stage.getRelativePointerPosition();
// Calculate image bounds on the stage // 计算图像在舞台上的边界
const imageX = (stageSize.width / canvasZoom - image.width) / 2; const imageX = (stageSize.width / canvasZoom - image.width) / 2;
const imageY = (stageSize.height / canvasZoom - image.height) / 2; const imageY = (stageSize.height / canvasZoom - image.height) / 2;
// Convert to image-relative coordinates // 转换为相对于图像的坐标
const relativeX = relativePos.x - imageX; const relativeX = relativePos.x - imageX;
const relativeY = relativePos.y - imageY; const relativeY = relativePos.y - imageY;
// Check if within image bounds // 检查是否在图像边界内
if (relativeX >= 0 && relativeX <= image.width && relativeY >= 0 && relativeY <= image.height) { if (relativeX >= 0 && relativeX <= image.width && relativeY >= 0 && relativeY <= image.height) {
setCurrentStroke([...currentStroke, relativeX, relativeY]); setCurrentStroke([...currentStroke, relativeX, relativeY]);
} }
@@ -174,10 +174,10 @@ export const ImageCanvas: React.FC = () => {
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* Toolbar */} {/* 工具栏 */}
<div className="p-3 border-b border-gray-800 bg-gray-950"> <div className="p-3 border-b border-gray-200 bg-white">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{/* Left side - Zoom controls */} {/* 左侧 - 缩放控制 */}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button variant="outline" size="sm" onClick={() => handleZoom(-0.1)}> <Button variant="outline" size="sm" onClick={() => handleZoom(-0.1)}>
<ZoomOut className="h-4 w-4" /> <ZoomOut className="h-4 w-4" />
@@ -193,12 +193,12 @@ export const ImageCanvas: React.FC = () => {
</Button> </Button>
</div> </div>
{/* Right side - Tools and actions */} {/* 右侧 - 工具和操作 */}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{selectedTool === 'mask' && ( {selectedTool === 'mask' && (
<> <>
<div className="flex items-center space-x-2 mr-2"> <div className="flex items-center space-x-2 mr-2">
<span className="text-xs text-gray-400">Brush:</span> <span className="text-xs text-gray-400">:</span>
<input <input
type="range" type="range"
min="5" min="5"
@@ -227,35 +227,35 @@ export const ImageCanvas: React.FC = () => {
className={cn(showMasks && 'bg-yellow-400/10 border-yellow-400/50')} className={cn(showMasks && 'bg-yellow-400/10 border-yellow-400/50')}
> >
{showMasks ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />} {showMasks ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
<span className="hidden sm:inline ml-2">Masks</span> <span className="hidden sm:inline ml-2"></span>
</Button> </Button>
{canvasImage && ( {canvasImage && (
<Button variant="secondary" size="sm" onClick={handleDownload}> <Button variant="secondary" size="sm" onClick={handleDownload}>
<Download className="h-4 w-4 mr-2" /> <Download className="h-4 w-4 mr-2" />
<span className="hidden sm:inline">Download</span> <span className="hidden sm:inline"></span>
</Button> </Button>
)} )}
</div> </div>
</div> </div>
</div> </div>
{/* Canvas Area */} {/* 画布区域 */}
<div <div
id="canvas-container" id="canvas-container"
className="flex-1 relative overflow-hidden bg-gray-800" className="flex-1 relative overflow-hidden bg-gray-100"
> >
{!image && !isGenerating && ( {!image && !isGenerating && (
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="text-6xl mb-4">🍌</div> <div className="text-6xl mb-4">🍌</div>
<h2 className="text-xl font-medium text-gray-300 mb-2"> <h2 className="text-xl font-medium text-gray-300 mb-2">
Welcome to Nano Banana Framework 使 Nano Banana
</h2> </h2>
<p className="text-gray-500 max-w-md"> <p className="text-gray-500 max-w-md">
{selectedTool === 'generate' {selectedTool === 'generate'
? 'Start by describing what you want to create in the prompt box' ? '首先在提示框中描述您想要创建的内容'
: 'Upload an image to begin editing' : '上传图像开始编辑'
} }
</p> </p>
</div> </div>
@@ -266,7 +266,7 @@ export const ImageCanvas: React.FC = () => {
<div className="absolute inset-0 flex items-center justify-center bg-gray-900/50"> <div className="absolute inset-0 flex items-center justify-center bg-gray-900/50">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-yellow-400 mb-4" /> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-yellow-400 mb-4" />
<p className="text-gray-300">Creating your image...</p> <p className="text-gray-300">...</p>
</div> </div>
</div> </div>
)} )}
@@ -302,7 +302,7 @@ export const ImageCanvas: React.FC = () => {
/> />
)} )}
{/* Brush Strokes */} {/* 画笔描边 */}
{showMasks && brushStrokes.map((stroke) => ( {showMasks && brushStrokes.map((stroke) => (
<Line <Line
key={stroke.id} key={stroke.id}
@@ -319,7 +319,7 @@ export const ImageCanvas: React.FC = () => {
/> />
))} ))}
{/* Current stroke being drawn */} {/* 正在绘制的当前描边 */}
{isDrawing && currentStroke.length > 2 && ( {isDrawing && currentStroke.length > 2 && (
<Line <Line
points={currentStroke} points={currentStroke}
@@ -338,12 +338,12 @@ export const ImageCanvas: React.FC = () => {
</Stage> </Stage>
</div> </div>
{/* Status Bar */} {/* 状态栏 */}
<div className="p-3 border-t border-gray-800 bg-gray-950"> <div className="p-3 border-t border-gray-200 bg-white">
<div className="flex items-center justify-between text-xs text-gray-500"> <div className="flex items-center justify-between text-xs text-gray-500">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
{brushStrokes.length > 0 && ( {brushStrokes.length > 0 && (
<span className="text-yellow-400">{brushStrokes.length} brush stroke{brushStrokes.length !== 1 ? 's' : ''}</span> <span className="text-yellow-400">{brushStrokes.length} {brushStrokes.length !== 1 ? 's' : ''}</span>
)} )}
</div> </div>
@@ -361,7 +361,7 @@ export const ImageCanvas: React.FC = () => {
</span> </span>
<span className="text-gray-600 hidden md:inline"></span> <span className="text-gray-600 hidden md:inline"></span>
<span className="text-yellow-400 hidden md:inline"></span> <span className="text-yellow-400 hidden md:inline"></span>
<span className="hidden md:inline">Powered by Gemini 2.5 Flash Image</span> <span className="hidden md:inline"> Gemini 2.5 Flash Image </span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -21,10 +21,10 @@ export const ImagePreviewModal: React.FC<ImagePreviewModalProps> = ({
return ( return (
<Dialog.Root open={open} onOpenChange={onOpenChange}> <Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/80 z-50" /> <Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
<Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-gray-900 border border-gray-700 rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto z-50"> <Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white border border-gray-200 rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto z-50">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<Dialog.Title className="text-lg font-semibold text-gray-100"> <Dialog.Title className="text-lg font-semibold text-gray-900">
{title} {title}
</Dialog.Title> </Dialog.Title>
<Dialog.Close asChild> <Dialog.Close asChild>
@@ -36,14 +36,14 @@ export const ImagePreviewModal: React.FC<ImagePreviewModalProps> = ({
<div className="space-y-4"> <div className="space-y-4">
{description && ( {description && (
<p className="text-sm text-gray-400">{description}</p> <p className="text-sm text-gray-600">{description}</p>
)} )}
<div className="bg-gray-800 rounded-lg p-4"> <div className="bg-gray-100 rounded-lg p-4">
<img <img
src={imageUrl} src={imageUrl}
alt={title} alt={title}
className="w-full h-auto rounded-lg border border-gray-700" className="w-full h-auto rounded-lg border border-gray-200"
/> />
</div> </div>
</div> </div>

View File

@@ -13,10 +13,10 @@ export const InfoModal: React.FC<InfoModalProps> = ({ open, onOpenChange }) => {
<Dialog.Root open={open} onOpenChange={onOpenChange}> <Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" /> <Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
<Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-gray-900 border border-gray-700 rounded-lg p-6 w-full max-w-4xl z-50"> <Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white border border-gray-200 rounded-lg p-6 w-full max-w-4xl z-50">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<Dialog.Title className="text-lg font-semibold text-gray-100"> <Dialog.Title className="text-lg font-semibold text-gray-900">
About Nano Banana AI Image Editor Nano Banana AI
</Dialog.Title> </Dialog.Title>
<Dialog.Close asChild> <Dialog.Close asChild>
<Button variant="ghost" size="icon" className="h-6 w-6"> <Button variant="ghost" size="icon" className="h-6 w-6">
@@ -26,59 +26,60 @@ export const InfoModal: React.FC<InfoModalProps> = ({ open, onOpenChange }) => {
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-3 text-sm text-gray-300"> <div className="space-y-3 text-sm text-gray-700">
<p> <p>
Developed by{' '} {' '}
<a <a
href="https://markfulton.com" href="https://markfulton.com"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-yellow-400 hover:text-yellow-300 transition-colors font-semibold" className="text-yellow-600 hover:text-yellow-700 transition-colors font-semibold"
> >
Mark Fulton Mark Fulton
<ExternalLink className="h-3 w-3 inline ml-1" /> <ExternalLink className="h-3 w-3 inline ml-1" />
</a> </a>
</p> </p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-4 bg-gradient-to-br from-purple-900/30 to-indigo-900/30 rounded-lg border border-purple-500/30"> <div className="p-4 bg-gradient-to-br from-purple-100 to-indigo-100 rounded-lg border border-purple-200">
<div className="flex items-center mb-3"> <div className="flex items-center mb-3">
<Lightbulb className="h-5 w-5 text-purple-400 mr-2" /> <Lightbulb className="h-5 w-5 text-purple-600 mr-2" />
<h4 className="text-sm font-semibold text-purple-300"> <h4 className="text-sm font-semibold text-purple-700">
Learn to Build AI Apps & More Solutions AI应用和其他解决方案
</h4> </h4>
</div> </div>
<p className="text-sm text-gray-300 mb-4"> <p className="text-sm text-gray-700 mb-4">
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自动化沿
</p> </p>
<a <a
href="https://www.reinventing.ai/" href="https://www.reinventing.ai/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center px-4 py-2 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white rounded-lg transition-all duration-200 font-medium" className="inline-flex items-center px-4 py-2 bg-gradient-to-r from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-600 text-white rounded-lg transition-all duration-200 font-medium"
> >
Join the AI Accelerator Program AI加速器计划
<ExternalLink className="h-4 w-4 ml-1" /> <ExternalLink className="h-4 w-4 ml-1" />
</a> </a>
</div> </div>
<div className="p-4 bg-gradient-to-br from-yellow-900/30 to-orange-900/30 rounded-lg border border-yellow-500/30"> <div className="p-4 bg-gradient-to-br from-yellow-100 to-orange-100 rounded-lg border border-yellow-200">
<div className="flex items-center mb-3"> <div className="flex items-center mb-3">
<Download className="h-5 w-5 text-yellow-400 mr-2" /> <Download className="h-5 w-5 text-yellow-600 mr-2" />
<h4 className="text-sm font-semibold text-yellow-300"> <h4 className="text-sm font-semibold text-yellow-700">
Get a Copy of This App
</h4> </h4>
</div> </div>
<p className="text-sm text-gray-300 mb-4"> <p className="text-sm text-gray-700 mb-4">
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社区获取此应用程序的副本
</p> </p>
<a <a
href="https://www.skool.com/vibe-coding-is-life/about?ref=456537abaf37491cbcc6976f3c26af41" href="https://www.skool.com/vibe-coding-is-life/about?ref=456537abaf37491cbcc6976f3c26af41"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center px-4 py-2 bg-gradient-to-r from-yellow-600 to-orange-600 hover:from-yellow-500 hover:to-orange-500 text-white rounded-lg transition-all duration-200 font-medium" className="inline-flex items-center px-4 py-2 bg-gradient-to-r from-yellow-500 to-orange-500 hover:from-yellow-600 hover:to-orange-600 text-white rounded-lg transition-all duration-200 font-medium"
> >
Join Vibe Coding is Life Community Vibe Coding is Life社区
<ExternalLink className="h-4 w-4 ml-1" /> <ExternalLink className="h-4 w-4 ml-1" />
</a> </a>
</div> </div>

View File

@@ -39,6 +39,7 @@ export const PromptComposer: React.FC = () => {
const [showAdvanced, setShowAdvanced] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false);
const [showClearConfirm, setShowClearConfirm] = useState(false); const [showClearConfirm, setShowClearConfirm] = useState(false);
const [showHintsModal, setShowHintsModal] = useState(false); const [showHintsModal, setShowHintsModal] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const handleGenerate = () => { const handleGenerate = () => {
@@ -60,39 +61,64 @@ export const PromptComposer: React.FC = () => {
} }
}; };
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileUpload = async (file: File) => {
const file = event.target.files?.[0];
if (file && file.type.startsWith('image/')) { if (file && file.type.startsWith('image/')) {
try { try {
const base64 = await blobToBase64(file); const base64 = await blobToBase64(file);
const dataUrl = `data:${file.type};base64,${base64}`; const dataUrl = `data:${file.type};base64,${base64}`;
if (selectedTool === 'generate') { if (selectedTool === 'generate') {
// Add to reference images (max 2) // 添加到参考图像最多2张
if (uploadedImages.length < 2) { if (uploadedImages.length < 2) {
addUploadedImage(dataUrl); addUploadedImage(dataUrl);
} }
} else if (selectedTool === 'edit') { } else if (selectedTool === 'edit') {
// For edit mode, add to separate edit reference images (max 2) // 编辑模式下添加到单独的编辑参考图像最多2张
if (editReferenceImages.length < 2) { if (editReferenceImages.length < 2) {
addEditReferenceImage(dataUrl); addEditReferenceImage(dataUrl);
} }
// Set as canvas image if none exists // 如果没有画布图像,则设置为画布图像
if (!canvasImage) { if (!canvasImage) {
setCanvasImage(dataUrl); setCanvasImage(dataUrl);
} }
} else if (selectedTool === 'mask') { } else if (selectedTool === 'mask') {
// For mask mode, set as canvas image immediately // 遮罩模式下,立即设置为画布图像
clearUploadedImages(); clearUploadedImages();
addUploadedImage(dataUrl); addUploadedImage(dataUrl);
setCanvasImage(dataUrl); setCanvasImage(dataUrl);
} }
} catch (error) { } catch (error) {
console.error('Failed to upload image:', error); console.error('上传图像失败:', error);
} }
} }
}; };
const handleFileInputChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
await handleFileUpload(file);
}
};
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(true);
};
const handleDragLeave = () => {
setIsDragOver(false);
};
const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(false);
const file = event.dataTransfer.files?.[0];
if (file) {
await handleFileUpload(file);
}
};
const handleClearSession = () => { const handleClearSession = () => {
setCurrentPrompt(''); setCurrentPrompt('');
clearUploadedImages(); clearUploadedImages();
@@ -105,18 +131,18 @@ export const PromptComposer: React.FC = () => {
}; };
const tools = [ const tools = [
{ id: 'generate', icon: Wand2, label: 'Generate', description: 'Create from text' }, { id: 'generate', icon: Wand2, label: '生成', description: '从文本创建' },
{ id: 'edit', icon: Edit3, label: 'Edit', description: 'Modify existing' }, { id: 'edit', icon: Edit3, label: '编辑', description: '修改现有图像' },
{ id: 'mask', icon: MousePointer, label: 'Select', description: 'Click to select' }, { id: 'mask', icon: MousePointer, label: '选择', description: '点击选择' },
] as const; ] as const;
if (!showPromptPanel) { if (!showPromptPanel) {
return ( return (
<div className="w-8 bg-gray-950 border-r border-gray-800 flex flex-col items-center justify-center"> <div className="w-8 bg-white border-r border-gray-200 flex flex-col items-center justify-center">
<button <button
onClick={() => setShowPromptPanel(true)} onClick={() => setShowPromptPanel(true)}
className="w-6 h-16 bg-gray-800 hover:bg-gray-700 rounded-r-lg border border-l-0 border-gray-700 flex items-center justify-center transition-colors group" className="w-6 h-16 bg-gray-100 hover:bg-gray-200 rounded-r-lg border border-l-0 border-gray-300 flex items-center justify-center transition-colors group"
title="Show Prompt Panel" title="显示提示面板"
> >
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full"></div> <div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full"></div>
@@ -130,10 +156,10 @@ export const PromptComposer: React.FC = () => {
return ( return (
<> <>
<div className="w-80 lg:w-72 xl:w-80 h-full bg-gray-950 border-r border-gray-800 p-6 flex flex-col space-y-6 overflow-y-auto"> <div className="w-80 lg:w-72 xl:w-80 h-full bg-white border-r border-gray-200 p-6 flex flex-col space-y-6 overflow-y-auto">
<div> <div>
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-gray-300">Mode</h3> <h3 className="text-sm font-medium text-gray-300"></h3>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<Button <Button
variant="ghost" variant="ghost"
@@ -148,7 +174,7 @@ export const PromptComposer: React.FC = () => {
size="icon" size="icon"
onClick={() => setShowPromptPanel(false)} onClick={() => setShowPromptPanel(false)}
className="h-6 w-6" className="h-6 w-6"
title="Hide Prompt Panel" title="隐藏提示面板"
> >
× ×
</Button> </Button>
@@ -163,7 +189,7 @@ export const PromptComposer: React.FC = () => {
'flex flex-col items-center p-3 rounded-lg border transition-all duration-200', 'flex flex-col items-center p-3 rounded-lg border transition-all duration-200',
selectedTool === tool.id selectedTool === tool.id
? 'bg-yellow-400/10 border-yellow-400/50 text-yellow-400' ? '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'
)} )}
> >
<tool.icon className="h-5 w-5 mb-1" /> <tool.icon className="h-5 w-5 mb-1" />
@@ -173,42 +199,69 @@ export const PromptComposer: React.FC = () => {
</div> </div>
</div> </div>
{/* File Upload */} {/* 文件上传 */}
<div> <div>
<div> <div
<label className="text-sm font-medium text-gray-300 mb-1 block"> onDragOver={handleDragOver}
{selectedTool === 'generate' ? 'Reference Images' : selectedTool === 'edit' ? 'Style References' : 'Upload Image'} onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={cn(
"border-2 border-dashed rounded-lg p-6 text-center transition-colors",
isDragOver
? "border-yellow-400 bg-yellow-400/10"
: "border-gray-300 hover:border-yellow-400"
)}
>
<label className="text-sm font-medium text-gray-700 mb-1 block">
{selectedTool === 'generate' ? '参考图像' : selectedTool === 'edit' ? '样式参考' : '上传图像'}
</label> </label>
{selectedTool === 'mask' && ( {selectedTool === 'mask' && (
<p className="text-xs text-gray-400 mb-3">Edit an image with masks</p> <p className="text-xs text-gray-500 mb-3">使</p>
)} )}
{selectedTool === 'generate' && ( {selectedTool === 'generate' && (
<p className="text-xs text-gray-500 mb-3">Optional, up to 2 images</p> <p className="text-xs text-gray-500 mb-3">2</p>
)} )}
{selectedTool === 'edit' && ( {selectedTool === 'edit' && (
<p className="text-xs text-gray-500 mb-3"> <p className="text-xs text-gray-500 mb-3">
{canvasImage ? 'Optional style references, up to 2 images' : 'Upload image to edit, up to 2 images'} {canvasImage ? '可选样式参考最多2张图像' : '上传要编辑的图像最多2张图像'}
</p> </p>
)} )}
<input <input
ref={fileInputRef} ref={fileInputRef}
type="file" type="file"
accept="image/*" accept="image/*"
onChange={handleFileUpload} onChange={handleFileInputChange}
className="hidden" className="hidden"
/> />
<div className="flex flex-col items-center justify-center space-y-3">
<Upload className={cn("h-8 w-8", isDragOver ? "text-yellow-500" : "text-gray-400")} />
<div>
<p className={cn(
"text-sm font-medium",
isDragOver ? "text-yellow-700" : "text-gray-600"
)}>
{isDragOver ? "释放文件以上传" : "拖拽图像到此处或点击上传"}
</p>
<p className="text-xs text-gray-500 mt-1">
JPG, PNG, GIF
</p>
</div>
<Button <Button
variant="outline" variant="outline"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
className="w-full" className="mt-2"
disabled={ disabled={
(selectedTool === 'generate' && uploadedImages.length >= 2) || (selectedTool === 'generate' && uploadedImages.length >= 2) ||
(selectedTool === 'edit' && editReferenceImages.length >= 2) (selectedTool === 'edit' && editReferenceImages.length >= 2)
} }
> >
<Upload className="h-4 w-4 mr-2" /> <Upload className="h-4 w-4 mr-2" />
Upload
</Button> </Button>
</div>
</div>
{/* Show uploaded images preview */} {/* Show uploaded images preview */}
{((selectedTool === 'generate' && uploadedImages.length > 0) || {((selectedTool === 'generate' && uploadedImages.length > 0) ||
@@ -218,45 +271,44 @@ export const PromptComposer: React.FC = () => {
<div key={index} className="relative"> <div key={index} className="relative">
<img <img
src={image} src={image}
alt={`Reference ${index + 1}`} alt={`参考图像 ${index + 1}`}
className="w-full h-20 object-cover rounded-lg border border-gray-700" className="w-full h-20 object-cover rounded-lg border border-gray-300"
/> />
<button <button
onClick={() => selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)} onClick={() => selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)}
className="absolute top-1 right-1 bg-gray-900/80 text-gray-400 hover:text-gray-200 rounded-full p-1 transition-colors" className="absolute top-1 right-1 bg-gray-100/80 text-gray-600 hover:text-gray-800 rounded-full p-1 transition-colors"
> >
× ×
</button> </button>
<div className="absolute bottom-1 left-1 bg-gray-900/80 text-xs px-2 py-1 rounded text-gray-300"> <div className="absolute bottom-1 left-1 bg-gray-100/80 text-xs px-2 py-1 rounded text-gray-700">
Ref {index + 1} {index + 1}
</div> </div>
</div> </div>
))} ))}
</div> </div>
)} )}
</div> </div>
</div>
{/* Prompt Input */} {/* 提示输入 */}
<div> <div>
<label className="text-sm font-medium text-gray-300 mb-3 block"> <label className="text-sm font-medium text-gray-700 mb-3 block">
{selectedTool === 'generate' ? 'Describe what you want to create' : 'Describe your changes'} {selectedTool === 'generate' ? '描述您想要创建的内容' : '描述您的修改'}
</label> </label>
<Textarea <Textarea
value={currentPrompt} value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)} onChange={(e) => setCurrentPrompt(e.target.value)}
placeholder={ placeholder={
selectedTool === 'generate' selectedTool === 'generate'
? 'A serene mountain landscape at sunset with a lake reflecting the golden sky...' ? '宁静的山景日落,湖面倒映着金色的天空...'
: 'Make the sky more dramatic, add storm clouds...' : '让天空更加戏剧化,添加暴风云...'
} }
className="min-h-[120px] resize-none" className="min-h-[120px] resize-none"
/> />
{/* Prompt Quality Indicator */} {/* 提示质量指示器 */}
<button <button
onClick={() => setShowHintsModal(true)} onClick={() => setShowHintsModal(true)}
className="mt-2 flex items-center text-xs hover:text-gray-400 transition-colors group" className="mt-2 flex items-center text-xs hover:text-gray-600 transition-colors group"
> >
{currentPrompt.length < 20 ? ( {currentPrompt.length < 20 ? (
<HelpCircle className="h-3 w-3 mr-2 text-red-500 group-hover:text-red-400" /> <HelpCircle className="h-3 w-3 mr-2 text-red-500 group-hover:text-red-400" />
@@ -266,15 +318,15 @@ export const PromptComposer: React.FC = () => {
currentPrompt.length < 50 ? 'bg-yellow-500' : 'bg-green-500' currentPrompt.length < 50 ? 'bg-yellow-500' : 'bg-green-500'
)} /> )} />
)} )}
<span className="text-gray-500 group-hover:text-gray-400"> <span className="text-gray-600 group-hover:text-gray-800">
{currentPrompt.length < 20 ? 'Add detail for better results' : {currentPrompt.length < 20 ? '添加更多细节以获得更好的结果' :
currentPrompt.length < 50 ? 'Good detail level' : 'Excellent prompt detail'} currentPrompt.length < 50 ? '细节水平良好' : '提示细节优秀'}
</span> </span>
</button> </button>
</div> </div>
{/* Generate Button */} {/* 生成按钮 */}
<Button <Button
onClick={handleGenerate} onClick={handleGenerate}
disabled={isGenerating || !currentPrompt.trim()} disabled={isGenerating || !currentPrompt.trim()}
@@ -283,38 +335,38 @@ export const PromptComposer: React.FC = () => {
{isGenerating ? ( {isGenerating ? (
<> <>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2" /> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2" />
Generating... ...
</> </>
) : ( ) : (
<> <>
<Wand2 className="h-4 w-4 mr-2" /> <Wand2 className="h-4 w-4 mr-2" />
{selectedTool === 'generate' ? 'Generate' : 'Apply Edit'} {selectedTool === 'generate' ? '生成' : '应用编辑'}
</> </>
)} )}
</Button> </Button>
{/* Advanced Controls */} {/* 高级控制 */}
<div> <div>
<button <button
onClick={() => setShowAdvanced(!showAdvanced)} onClick={() => setShowAdvanced(!showAdvanced)}
className="flex items-center text-sm text-gray-400 hover:text-gray-300 transition-colors duration-200" className="flex items-center text-sm text-gray-600 hover:text-gray-800 transition-colors duration-200"
> >
{showAdvanced ? <ChevronDown className="h-4 w-4 mr-1" /> : <ChevronRight className="h-4 w-4 mr-1" />} {showAdvanced ? <ChevronDown className="h-4 w-4 mr-1" /> : <ChevronRight className="h-4 w-4 mr-1" />}
{showAdvanced ? 'Hide' : 'Show'} Advanced Controls {showAdvanced ? '隐藏' : '显示'}
</button> </button>
<button <button
onClick={() => setShowClearConfirm(!showClearConfirm)} onClick={() => setShowClearConfirm(!showClearConfirm)}
className="flex items-center text-sm text-gray-400 hover:text-red-400 transition-colors duration-200 mt-2" className="flex items-center text-sm text-gray-600 hover:text-red-500 transition-colors duration-200 mt-2"
> >
<RotateCcw className="h-4 w-4 mr-2" /> <RotateCcw className="h-4 w-4 mr-2" />
Clear Session
</button> </button>
{showClearConfirm && ( {showClearConfirm && (
<div className="mt-3 p-3 bg-gray-800 rounded-lg border border-gray-700"> <div className="mt-3 p-3 bg-gray-100 rounded-lg border border-gray-300">
<p className="text-xs text-gray-300 mb-3"> <p className="text-xs text-gray-600 mb-3">
Are you sure you want to clear this session? This will remove all uploads, prompts, and canvas content.
</p> </p>
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button <Button
@@ -323,7 +375,7 @@ export const PromptComposer: React.FC = () => {
onClick={handleClearSession} onClick={handleClearSession}
className="flex-1" className="flex-1"
> >
Yes, Clear
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -331,7 +383,7 @@ export const PromptComposer: React.FC = () => {
onClick={() => setShowClearConfirm(false)} onClick={() => setShowClearConfirm(false)}
className="flex-1" className="flex-1"
> >
Cancel
</Button> </Button>
</div> </div>
</div> </div>
@@ -339,10 +391,10 @@ export const PromptComposer: React.FC = () => {
{showAdvanced && ( {showAdvanced && (
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">
{/* Temperature */} {/* 创造力 */}
<div> <div>
<label className="text-xs text-gray-400 mb-2 block"> <label className="text-xs text-gray-600 mb-2 block">
Creativity ({temperature}) ({temperature})
</label> </label>
<input <input
type="range" type="range"
@@ -351,55 +403,55 @@ export const PromptComposer: React.FC = () => {
step="0.1" step="0.1"
value={temperature} value={temperature}
onChange={(e) => setTemperature(parseFloat(e.target.value))} onChange={(e) => setTemperature(parseFloat(e.target.value))}
className="w-full h-2 bg-gray-800 rounded-lg appearance-none cursor-pointer slider" className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
/> />
</div> </div>
{/* Seed */} {/* 种子 */}
<div> <div>
<label className="text-xs text-gray-400 mb-2 block"> <label className="text-xs text-gray-600 mb-2 block">
Seed (optional) ()
</label> </label>
<input <input
type="number" type="number"
value={seed || ''} value={seed || ''}
onChange={(e) => setSeed(e.target.value ? parseInt(e.target.value) : null)} onChange={(e) => setSeed(e.target.value ? parseInt(e.target.value) : null)}
placeholder="Random" placeholder="随机"
className="w-full h-8 px-2 bg-gray-900 border border-gray-700 rounded text-xs text-gray-100" className="w-full h-8 px-2 bg-white border border-gray-300 rounded text-xs text-gray-900"
/> />
</div> </div>
</div> </div>
)} )}
</div> </div>
{/* Keyboard Shortcuts */} {/* 键盘快捷键 */}
<div className="pt-4 border-t border-gray-800"> <div className="pt-4 border-t border-gray-200">
<h4 className="text-xs font-medium text-gray-400 mb-2">Shortcuts</h4> <h4 className="text-xs font-medium text-gray-600 mb-2"></h4>
<div className="space-y-1 text-xs text-gray-500"> <div className="space-y-1 text-xs text-gray-700">
<div className="flex justify-between"> <div className="flex justify-between">
<span>Generate</span> <span></span>
<span> + Enter</span> <span> + Enter</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>Re-roll</span> <span></span>
<span> + R</span> <span> + R</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>Edit mode</span> <span></span>
<span>E</span> <span>E</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>History</span> <span></span>
<span>H</span> <span>H</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>Toggle Panel</span> <span></span>
<span>P</span> <span>P</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Prompt Hints Modal */} {/* 提示提示模态框 */}
<PromptHints open={showHintsModal} onOpenChange={setShowHintsModal} /> <PromptHints open={showHintsModal} onOpenChange={setShowHintsModal} />
</> </>
); );

View File

@@ -49,11 +49,11 @@ export const PromptHints: React.FC<PromptHintsProps> = ({ open, onOpenChange })
return ( return (
<Dialog.Root open={open} onOpenChange={onOpenChange}> <Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" /> <Dialog.Overlay className="fixed inset-0 bg-black/30 z-50" />
<Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-gray-900 border border-gray-700 rounded-lg p-6 w-full max-w-md max-h-[80vh] overflow-y-auto z-50"> <Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white border border-gray-200 rounded-lg p-6 w-full max-w-md max-h-[80vh] overflow-y-auto z-50">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<Dialog.Title className="text-lg font-semibold text-gray-100"> <Dialog.Title className="text-lg font-semibold text-gray-900">
Prompt Quality Tips
</Dialog.Title> </Dialog.Title>
<Dialog.Close asChild> <Dialog.Close asChild>
<Button variant="ghost" size="icon" className="h-6 w-6"> <Button variant="ghost" size="icon" className="h-6 w-6">
@@ -68,15 +68,15 @@ export const PromptHints: React.FC<PromptHintsProps> = ({ open, onOpenChange })
<div className={`inline-block px-2 py-1 rounded text-xs border ${categoryColors[hint.category]}`}> <div className={`inline-block px-2 py-1 rounded text-xs border ${categoryColors[hint.category]}`}>
{hint.category} {hint.category}
</div> </div>
<p className="text-sm text-gray-300">{hint.text}</p> <p className="text-sm text-gray-700">{hint.text}</p>
<p className="text-sm text-gray-500 italic">{hint.example}</p> <p className="text-sm text-gray-500 italic">{hint.example}</p>
</div> </div>
))} ))}
<div className="p-4 bg-gray-800 rounded-lg border border-gray-700 mt-6"> <div className="p-4 bg-gray-100 rounded-lg border border-gray-200 mt-6">
<p className="text-sm text-gray-300"> <p className="text-sm text-gray-700">
<strong className="text-yellow-400">Best practice:</strong> Write full sentences that describe the complete scene, <strong className="text-yellow-600">:</strong>
not just keywords. Think "paint me a picture with words." "用文字为我画一幅画"
</p> </p>
</div> </div>
</div> </div>

View File

@@ -8,10 +8,10 @@ const buttonVariants = cva(
variants: { variants: {
variant: { variant: {
default: 'bg-yellow-400 text-gray-900 hover:bg-yellow-300 focus-visible:ring-yellow-400', default: 'bg-yellow-400 text-gray-900 hover:bg-yellow-300 focus-visible:ring-yellow-400',
secondary: 'bg-gray-800 text-gray-100 hover:bg-gray-700 focus-visible:ring-gray-600', secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-300',
outline: 'border border-gray-600 bg-transparent text-gray-300 hover:bg-gray-800 hover:text-gray-100', outline: 'border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-100 hover:text-gray-900',
ghost: 'text-gray-400 hover:bg-gray-800 hover:text-gray-100', ghost: 'text-gray-500 hover:bg-gray-100 hover:text-gray-900',
destructive: 'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500', destructive: 'bg-red-500 text-white hover:bg-red-600 focus-visible:ring-red-400',
}, },
size: { size: {
default: 'h-10 px-4 py-2', default: 'h-10 px-4 py-2',

View File

@@ -9,7 +9,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input <input
type={type} type={type}
className={cn( className={cn(
'flex h-10 w-full rounded-lg border border-gray-600 bg-gray-900 px-3 py-2 text-sm text-gray-100 ring-offset-gray-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-yellow-400 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', 'flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-yellow-400 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className className
)} )}
ref={ref} ref={ref}

View File

@@ -8,7 +8,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return ( return (
<textarea <textarea
className={cn( className={cn(
'flex min-h-[80px] w-full rounded-lg border border-gray-600 bg-gray-900 px-3 py-2 text-sm text-gray-100 ring-offset-gray-900 placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-yellow-400 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none', 'flex min-h-[80px] w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 ring-offset-white placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-yellow-400 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none',
className className
)} )}
ref={ref} ref={ref}

View File

@@ -22,9 +22,9 @@ export const useImageGeneration = () => {
type: 'output', type: 'output',
url: `data:image/png;base64,${base64}`, url: `data:image/png;base64,${base64}`,
mime: 'image/png', mime: 'image/png',
width: 1024, // Default Gemini output size width: 1024, // 默认Gemini输出尺寸
height: 1024, height: 1024,
checksum: base64.slice(0, 32) // Simple checksum checksum: base64.slice(0, 32) // 简单校验和
})); }));
const generation: Generation = { const generation: Generation = {
@@ -60,11 +60,11 @@ export const useImageGeneration = () => {
addGeneration(generation); addGeneration(generation);
setCanvasImage(outputAssets[0].url); setCanvasImage(outputAssets[0].url);
// Create project if none exists // 如果没有项目则创建项目
if (!currentProject) { if (!currentProject) {
const newProject = { const newProject = {
id: generateId(), id: generateId(),
title: 'Untitled Project', title: '未命名项目',
generations: [generation], generations: [generation],
edits: [], edits: [],
createdAt: Date.now(), createdAt: Date.now(),
@@ -76,7 +76,7 @@ export const useImageGeneration = () => {
setIsGenerating(false); setIsGenerating(false);
}, },
onError: (error) => { onError: (error) => {
console.error('Generation failed:', error); console.error('生成失败:', error);
setIsGenerating(false); setIsGenerating(false);
} }
}); });
@@ -104,16 +104,16 @@ export const useImageEditing = () => {
const editMutation = useMutation({ const editMutation = useMutation({
mutationFn: async (instruction: string) => { mutationFn: async (instruction: string) => {
// Always use canvas image as primary target if available, otherwise use first uploaded image // 如果可用,始终使用画布图像作为主要目标,否则使用第一张上传的图像
const sourceImage = canvasImage || uploadedImages[0]; const sourceImage = canvasImage || uploadedImages[0];
if (!sourceImage) throw new Error('No image to edit'); if (!sourceImage) throw new Error('没有要编辑的图像');
// Convert canvas image to base64 // 将画布图像转换为base64
const base64Image = sourceImage.includes('base64,') const base64Image = sourceImage.includes('base64,')
? sourceImage.split('base64,')[1] ? sourceImage.split('base64,')[1]
: sourceImage; : sourceImage;
// Get reference images for style guidance // 获取用于样式指导的参考图像
let referenceImages = editReferenceImages let referenceImages = editReferenceImages
.filter(img => img.includes('base64,')) .filter(img => img.includes('base64,'))
.map(img => img.split('base64,')[1]); .map(img => img.split('base64,')[1]);
@@ -121,26 +121,26 @@ export const useImageEditing = () => {
let maskImage: string | undefined; let maskImage: string | undefined;
let maskedReferenceImage: string | undefined; let maskedReferenceImage: string | undefined;
// Create mask from brush strokes if any exist // 如果存在画笔描边,则从描边创建遮罩
if (brushStrokes.length > 0) { if (brushStrokes.length > 0) {
// Create a temporary image to get actual dimensions // 创建临时图像以获取实际尺寸
const tempImg = new Image(); const tempImg = new Image();
tempImg.src = sourceImage; tempImg.src = sourceImage;
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
tempImg.onload = () => resolve(); tempImg.onload = () => resolve();
}); });
// Create mask canvas with exact image dimensions // 创建具有确切图像尺寸的遮罩画布
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!; const ctx = canvas.getContext('2d')!;
canvas.width = tempImg.width; canvas.width = tempImg.width;
canvas.height = tempImg.height; canvas.height = tempImg.height;
// Fill with black (unmasked areas) // 用黑色填充(未遮罩区域)
ctx.fillStyle = 'black'; ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw white strokes (masked areas) // 绘制白色描边(遮罩区域)
ctx.strokeStyle = 'white'; ctx.strokeStyle = 'white';
ctx.lineCap = 'round'; ctx.lineCap = 'round';
ctx.lineJoin = 'round'; ctx.lineJoin = 'round';
@@ -158,20 +158,20 @@ export const useImageEditing = () => {
} }
}); });
// Convert mask to base64 // 将遮罩转换为base64
const maskDataUrl = canvas.toDataURL('image/png'); const maskDataUrl = canvas.toDataURL('image/png');
maskImage = maskDataUrl.split('base64,')[1]; maskImage = maskDataUrl.split('base64,')[1];
// Create masked reference image (original image with mask overlay) // 创建遮罩参考图像(带遮罩叠加的原始图像)
const maskedCanvas = document.createElement('canvas'); const maskedCanvas = document.createElement('canvas');
const maskedCtx = maskedCanvas.getContext('2d')!; const maskedCtx = maskedCanvas.getContext('2d')!;
maskedCanvas.width = tempImg.width; maskedCanvas.width = tempImg.width;
maskedCanvas.height = tempImg.height; maskedCanvas.height = tempImg.height;
// Draw original image // 绘制原始图像
maskedCtx.drawImage(tempImg, 0, 0); maskedCtx.drawImage(tempImg, 0, 0);
// Draw mask overlay with transparency // 绘制带透明度的遮罩叠加
maskedCtx.globalCompositeOperation = 'source-over'; maskedCtx.globalCompositeOperation = 'source-over';
maskedCtx.globalAlpha = 0.4; maskedCtx.globalAlpha = 0.4;
maskedCtx.fillStyle = '#A855F7'; maskedCtx.fillStyle = '#A855F7';
@@ -198,7 +198,7 @@ export const useImageEditing = () => {
const maskedDataUrl = maskedCanvas.toDataURL('image/png'); const maskedDataUrl = maskedCanvas.toDataURL('image/png');
maskedReferenceImage = maskedDataUrl.split('base64,')[1]; maskedReferenceImage = maskedDataUrl.split('base64,')[1];
// Add the masked image as a reference for the model // 将遮罩图像作为参考添加到模型中
referenceImages = [maskedReferenceImage, ...referenceImages]; referenceImages = [maskedReferenceImage, ...referenceImages];
} }
@@ -229,7 +229,7 @@ export const useImageEditing = () => {
checksum: base64.slice(0, 32) checksum: base64.slice(0, 32)
})); }));
// Create mask reference asset if we have one // 如果有遮罩参考图像则创建遮罩参考资产
const maskReferenceAsset: Asset | undefined = maskedReferenceImage ? { const maskReferenceAsset: Asset | undefined = maskedReferenceImage ? {
id: generateId(), id: generateId(),
type: 'mask', type: 'mask',
@@ -252,7 +252,7 @@ export const useImageEditing = () => {
addEdit(edit); addEdit(edit);
// Automatically load the edited image in the canvas // 自动在画布中加载编辑后的图像
const { selectEdit, selectGeneration } = useAppStore.getState(); const { selectEdit, selectGeneration } = useAppStore.getState();
setCanvasImage(outputAssets[0].url); setCanvasImage(outputAssets[0].url);
selectEdit(edit.id); selectEdit(edit.id);
@@ -261,7 +261,7 @@ export const useImageEditing = () => {
setIsGenerating(false); setIsGenerating(false);
}, },
onError: (error) => { onError: (error) => {
console.error('Edit failed:', error); console.error('编辑失败:', error);
setIsGenerating(false); setIsGenerating(false);
} }
}); });

View File

@@ -11,25 +11,27 @@ body {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
overflow: hidden; overflow: hidden;
background-color: #FFFFFF;
color: #212529;
} }
/* Custom scrollbar for dark theme */ /* Custom scrollbar for light theme */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 6px;
height: 6px; height: 6px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: rgb(17 24 39); background: #F8F9FA;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgb(75 85 99); background: #E9ECEF;
border-radius: 3px; border-radius: 3px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: rgb(107 114 128); background: #DEE2E6;
} }
/* Custom range slider styling */ /* Custom range slider styling */
@@ -40,7 +42,7 @@ body {
border-radius: 50%; border-radius: 50%;
background: #FDE047; background: #FDE047;
cursor: pointer; cursor: pointer;
border: 2px solid #1F2937; border: 2px solid #FFFFFF;
} }
.slider::-moz-range-thumb { .slider::-moz-range-thumb {
@@ -49,7 +51,7 @@ body {
border-radius: 50%; border-radius: 50%;
background: #FDE047; background: #FDE047;
cursor: pointer; cursor: pointer;
border: 2px solid #1F2937; border: 2px solid #FFFFFF;
} }
/* Marching ants animation */ /* Marching ants animation */

View File

@@ -1,12 +1,12 @@
import { GoogleGenAI } from '@google/genai'; import { GoogleGenAI } from '@google/genai';
// Note: In production, this should be handled via a backend proxy // 注意:在生产环境中,这应该通过后端代理处理
const API_KEY = import.meta.env.VITE_GEMINI_API_KEY || 'demo-key'; const API_KEY = import.meta.env.VITE_GEMINI_API_KEY || 'demo-key';
const genAI = new GoogleGenAI({ apiKey: API_KEY }); const genAI = new GoogleGenAI({ apiKey: API_KEY });
export interface GenerationRequest { export interface GenerationRequest {
prompt: string; prompt: string;
referenceImages?: string[]; // base64 array referenceImages?: string[]; // base64数组
temperature?: number; temperature?: number;
seed?: number; seed?: number;
} }
@@ -14,7 +14,7 @@ export interface GenerationRequest {
export interface EditRequest { export interface EditRequest {
instruction: string; instruction: string;
originalImage: string; // base64 originalImage: string; // base64
referenceImages?: string[]; // base64 array referenceImages?: string[]; // base64数组
maskImage?: string; // base64 maskImage?: string; // base64
temperature?: number; temperature?: number;
seed?: number; seed?: number;
@@ -22,7 +22,7 @@ export interface EditRequest {
export interface SegmentationRequest { export interface SegmentationRequest {
image: string; // base64 image: string; // base64
query: string; // "the object at pixel (x,y)" or "the red car" query: string; // "像素(x,y)处的对象" 或 "红色汽车"
} }
export class GeminiService { export class GeminiService {
@@ -30,7 +30,7 @@ export class GeminiService {
try { try {
const contents: any[] = [{ text: request.prompt }]; const contents: any[] = [{ text: request.prompt }];
// Add reference images if provided // 如果提供了参考图像则添加
if (request.referenceImages && request.referenceImages.length > 0) { if (request.referenceImages && request.referenceImages.length > 0) {
request.referenceImages.forEach(image => { request.referenceImages.forEach(image => {
contents.push({ contents.push({
@@ -57,8 +57,8 @@ export class GeminiService {
return images; return images;
} catch (error) { } catch (error) {
console.error('Error generating image:', error); console.error('生成图像时出错:', error);
throw new Error('Failed to generate image. Please try again.'); throw new Error('生成图像失败。请重试。');
} }
} }
@@ -74,7 +74,7 @@ export class GeminiService {
}, },
]; ];
// Add reference images if provided // 如果提供了参考图像则添加
if (request.referenceImages && request.referenceImages.length > 0) { if (request.referenceImages && request.referenceImages.length > 0) {
request.referenceImages.forEach(image => { request.referenceImages.forEach(image => {
contents.push({ contents.push({
@@ -110,28 +110,28 @@ export class GeminiService {
return images; return images;
} catch (error) { } catch (error) {
console.error('Error editing image:', error); console.error('编辑图像时出错:', error);
throw new Error('Failed to edit image. Please try again.'); throw new Error('编辑图像失败。请重试。');
} }
} }
async segmentImage(request: SegmentationRequest): Promise<any> { async segmentImage(request: SegmentationRequest): Promise<any> {
try { try {
const prompt = [ const prompt = [
{ text: `Analyze this image and create a segmentation mask for: ${request.query} { text: `分析此图像并为以下对象创建分割遮罩: ${request.query}
Return a JSON object with this exact structure: 返回具有此确切结构的JSON对象:
{ {
"masks": [ "masks": [
{ {
"label": "description of the segmented object", "label": "分割对象的描述",
"box_2d": [x, y, width, height], "box_2d": [x, y, width, height],
"mask": "base64-encoded binary mask image" "mask": "base64编码的二进制遮罩图像"
} }
] ]
} }
Only segment the specific object or region requested. The mask should be a binary PNG where white pixels (255) indicate the selected region and black pixels (0) indicate the background.` }, 仅分割请求的特定对象或区域。遮罩应该是二进制PNG其中白色像素(255)表示选定区域,黑色像素(0)表示背景。` },
{ {
inlineData: { inlineData: {
mimeType: "image/png", mimeType: "image/png",
@@ -148,21 +148,21 @@ Only segment the specific object or region requested. The mask should be a binar
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 segmenting image:', error); console.error('分割图像时出错:', error);
throw new Error('Failed to segment image. Please try again.'); throw new Error('分割图像失败。请重试。');
} }
} }
private buildEditPrompt(request: EditRequest): string { private buildEditPrompt(request: EditRequest): string {
const maskInstruction = request.maskImage const maskInstruction = request.maskImage
? "\n\nIMPORTANT: Apply changes ONLY where the mask image shows white pixels (value 255). Leave all other areas completely unchanged. Respect the mask boundaries precisely and maintain seamless blending at the edges." ? "\n\n重要: 仅在遮罩图像显示白色像素(值255)的地方应用更改。完全不更改所有其他区域。精确遵守遮罩边界并在边缘保持无缝混合。"
: ""; : "";
return `Edit this image according to the following instruction: ${request.instruction} return `根据以下指令编辑此图像: ${request.instruction}
Maintain the original image's lighting, perspective, and overall composition. Make the changes look natural and seamlessly integrated.${maskInstruction} 保持原始图像的光照、透视和整体构图。使更改看起来自然且无缝集成。${maskInstruction}
Preserve image quality and ensure the edit looks professional and realistic.`; 保持图像质量并确保编辑看起来专业且逼真。`;
} }
} }

View File

@@ -3,41 +3,41 @@ import { devtools } from 'zustand/middleware';
import { Project, Generation, Edit, SegmentationMask, BrushStroke } from '../types'; import { Project, Generation, Edit, SegmentationMask, BrushStroke } from '../types';
interface AppState { interface AppState {
// Current project // 当前项目
currentProject: Project | null; currentProject: Project | null;
// Canvas state // 画布状态
canvasImage: string | null; canvasImage: string | null;
canvasZoom: number; canvasZoom: number;
canvasPan: { x: number; y: number }; canvasPan: { x: number; y: number };
// Upload state // 上传状态
uploadedImages: string[]; uploadedImages: string[];
editReferenceImages: string[]; editReferenceImages: string[];
// Brush strokes for painting masks // 用于绘制遮罩的画笔描边
brushStrokes: BrushStroke[]; brushStrokes: BrushStroke[];
brushSize: number; brushSize: number;
showMasks: boolean; showMasks: boolean;
// Generation state // 生成状态
isGenerating: boolean; isGenerating: boolean;
currentPrompt: string; currentPrompt: string;
temperature: number; temperature: number;
seed: number | null; seed: number | null;
// History and variants // 历史记录和变体
selectedGenerationId: string | null; selectedGenerationId: string | null;
selectedEditId: string | null; selectedEditId: string | null;
showHistory: boolean; showHistory: boolean;
// Panel visibility // 面板可见性
showPromptPanel: boolean; showPromptPanel: boolean;
// UI state // UI状态
selectedTool: 'generate' | 'edit' | 'mask'; selectedTool: 'generate' | 'edit' | 'mask';
// Actions // 操作
setCurrentProject: (project: Project | null) => void; setCurrentProject: (project: Project | null) => void;
setCanvasImage: (url: string | null) => void; setCanvasImage: (url: string | null) => void;
setCanvasZoom: (zoom: number) => void; setCanvasZoom: (zoom: number) => void;
@@ -75,7 +75,7 @@ interface AppState {
export const useAppStore = create<AppState>()( export const useAppStore = create<AppState>()(
devtools( devtools(
(set, get) => ({ (set, get) => ({
// Initial state // 初始状态
currentProject: null, currentProject: null,
canvasImage: null, canvasImage: null,
canvasZoom: 1, canvasZoom: 1,
@@ -101,7 +101,7 @@ export const useAppStore = create<AppState>()(
selectedTool: 'generate', selectedTool: 'generate',
// Actions // 操作
setCurrentProject: (project) => set({ currentProject: project }), setCurrentProject: (project) => set({ currentProject: project }),
setCanvasImage: (url) => set({ canvasImage: url }), setCanvasImage: (url) => set({ canvasImage: url }),
setCanvasZoom: (zoom) => set({ canvasZoom: zoom }), setCanvasZoom: (zoom) => set({ canvasZoom: zoom }),

View File

@@ -17,6 +17,14 @@ export default {
800: '#854D0E', 800: '#854D0E',
900: '#713F12', 900: '#713F12',
}, },
// Light theme colors
light: {
background: '#FFFFFF',
panel: '#F8F9FA',
border: '#E9ECEF',
text: '#212529',
textSecondary: '#6C757D',
}
}, },
fontFamily: { fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'], sans: ['Inter', 'system-ui', 'sans-serif'],