You've already forked Nano-Banana-AI-Image-Editor
初始化提交
This commit is contained in:
@@ -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
|
|
||||||
24
index.html
24
index.html
@@ -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" />
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }))}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
</label>
|
onDrop={handleDrop}
|
||||||
{selectedTool === 'mask' && (
|
className={cn(
|
||||||
<p className="text-xs text-gray-400 mb-3">Edit an image with masks</p>
|
"border-2 border-dashed rounded-lg p-6 text-center transition-colors",
|
||||||
)}
|
isDragOver
|
||||||
{selectedTool === 'generate' && (
|
? "border-yellow-400 bg-yellow-400/10"
|
||||||
<p className="text-xs text-gray-500 mb-3">Optional, up to 2 images</p>
|
: "border-gray-300 hover:border-yellow-400"
|
||||||
)}
|
)}
|
||||||
{selectedTool === 'edit' && (
|
|
||||||
<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'}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleFileUpload}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
className="w-full"
|
|
||||||
disabled={
|
|
||||||
(selectedTool === 'generate' && uploadedImages.length >= 2) ||
|
|
||||||
(selectedTool === 'edit' && editReferenceImages.length >= 2)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Upload className="h-4 w-4 mr-2" />
|
<label className="text-sm font-medium text-gray-700 mb-1 block">
|
||||||
Upload
|
{selectedTool === 'generate' ? '参考图像' : selectedTool === 'edit' ? '样式参考' : '上传图像'}
|
||||||
</Button>
|
</label>
|
||||||
|
{selectedTool === 'mask' && (
|
||||||
|
<p className="text-xs text-gray-500 mb-3">使用遮罩编辑图像</p>
|
||||||
|
)}
|
||||||
|
{selectedTool === 'generate' && (
|
||||||
|
<p className="text-xs text-gray-500 mb-3">可选,最多2张图像</p>
|
||||||
|
)}
|
||||||
|
{selectedTool === 'edit' && (
|
||||||
|
<p className="text-xs text-gray-500 mb-3">
|
||||||
|
{canvasImage ? '可选样式参考,最多2张图像' : '上传要编辑的图像,最多2张图像'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleFileInputChange}
|
||||||
|
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
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className="mt-2"
|
||||||
|
disabled={
|
||||||
|
(selectedTool === 'generate' && uploadedImages.length >= 2) ||
|
||||||
|
(selectedTool === 'edit' && editReferenceImages.length >= 2)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Upload className="h-4 w-4 mr-2" />
|
||||||
|
选择文件
|
||||||
|
</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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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.`;
|
保持图像质量并确保编辑看起来专业且逼真。`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
Reference in New Issue
Block a user