新增 历史记录持久化功能;

新增 增加最大历史记录条数为10条;
This commit is contained in:
2025-09-14 07:14:16 +08:00
parent 92a78abd63
commit f2f9e4a239
3 changed files with 525 additions and 230 deletions

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { useAppStore } from '../store/useAppStore'; import { useAppStore } from '../store/useAppStore';
import { Button } from './ui/Button'; import { Button } from './ui/Button';
import { History, Download, Image as ImageIcon, Layers } from 'lucide-react'; import { History, Download, Image as ImageIcon } from 'lucide-react';
import { cn } from '../utils/cn'; import { cn } from '../utils/cn';
import { ImagePreviewModal } from './ImagePreviewModal'; import { ImagePreviewModal } from './ImagePreviewModal';
@@ -19,6 +19,8 @@ export const HistoryPanel: React.FC = () => {
selectedTool selectedTool
} = useAppStore(); } = useAppStore();
const { getBlob } = useAppStore.getState();
const [previewModal, setPreviewModal] = React.useState<{ const [previewModal, setPreviewModal] = React.useState<{
open: boolean; open: boolean;
imageUrl: string; imageUrl: string;
@@ -31,6 +33,9 @@ export const HistoryPanel: React.FC = () => {
description: '' description: ''
}); });
// 存储从Blob URL解码的图像数据
const [decodedImages, setDecodedImages] = useState<Record<string, string>>({});
const generations = currentProject?.generations || []; const generations = currentProject?.generations || [];
const edits = currentProject?.edits || []; const edits = currentProject?.edits || [];
@@ -49,6 +54,57 @@ export const HistoryPanel: React.FC = () => {
} }
}, [canvasImage]); }, [canvasImage]);
// 当项目变化时解码Blob图像
useEffect(() => {
const decodeBlobImages = async () => {
const newDecodedImages: Record<string, string> = {};
// 解码生成记录的输出图像
for (const gen of generations) {
if (Array.isArray(gen.outputAssetsBlobUrls)) {
for (const blobUrl of gen.outputAssetsBlobUrls) {
if (!decodedImages[blobUrl] && blobUrl.startsWith('blob:')) {
const blob = getBlob(blobUrl);
if (blob) {
const dataUrl = await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(blob);
});
newDecodedImages[blobUrl] = dataUrl;
}
}
}
}
}
// 解码编辑记录的输出图像
for (const edit of edits) {
if (Array.isArray(edit.outputAssetsBlobUrls)) {
for (const blobUrl of edit.outputAssetsBlobUrls) {
if (!decodedImages[blobUrl] && blobUrl.startsWith('blob:')) {
const blob = getBlob(blobUrl);
if (blob) {
const dataUrl = await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(blob);
});
newDecodedImages[blobUrl] = dataUrl;
}
}
}
}
}
if (Object.keys(newDecodedImages).length > 0) {
setDecodedImages(prev => ({ ...prev, ...newDecodedImages }));
}
};
decodeBlobImages();
}, [generations, edits, getBlob, decodedImages]);
if (!showHistory) { if (!showHistory) {
return ( return (
<div className="w-8 bg-white border-l border-gray-200 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">
@@ -88,85 +144,124 @@ export const HistoryPanel: React.FC = () => {
{/* 变体网格 */} {/* 变体网格 */}
<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"></h4> <div className="flex items-center justify-between mb-3">
<h4 className="text-xs font-medium text-gray-400"></h4>
<span className="text-xs text-gray-500">
{generations.length + edits.length}/10
</span>
</div>
{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"></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 max-h-80 overflow-y-auto">
{/* 显示生成记录 */} {/* 显示生成记录 */}
{generations.slice(-2).map((generation, index) => ( {[...generations].sort((a, b) => b.timestamp - a.timestamp).slice(0, 10).map((generation, index) => (
<div <div
key={generation.id} key={generation.id}
className={cn( className={cn(
'relative aspect-square rounded-lg border-2 cursor-pointer transition-all duration-200 overflow-hidden', 'relative aspect-square rounded-lg border-2 cursor-pointer transition-all duration-200 overflow-hidden',
selectedGenerationId === generation.id selectedGenerationId === generation.id
? 'border-yellow-400' ? 'border-yellow-400'
: 'border-gray-700 hover:border-gray-600' : 'border-gray-300 hover:border-gray-400'
)} )}
onClick={() => { onClick={() => {
selectGeneration(generation.id); selectGeneration(generation.id);
if (generation.outputAssets[0]) { // 设置画布图像为第一个输出资产
setCanvasImage(generation.outputAssets[0].url); if (generation.outputAssetsBlobUrls && generation.outputAssetsBlobUrls.length > 0) {
const blobUrl = generation.outputAssetsBlobUrls[0];
const decodedUrl = decodedImages[blobUrl];
if (decodedUrl) {
setCanvasImage(decodedUrl);
} else if (!blobUrl.startsWith('blob:')) {
// 如果不是Blob URL直接使用
setCanvasImage(blobUrl);
}
} }
}} }}
> >
{generation.outputAssets[0] ? ( {generation.outputAssetsBlobUrls && generation.outputAssetsBlobUrls.length > 0 ? (
<> (() => {
<img const blobUrl = generation.outputAssetsBlobUrls[0];
src={generation.outputAssets[0].url} const decodedUrl = decodedImages[blobUrl];
alt="生成的变体" if (decodedUrl) {
className="w-full h-full object-cover" return <img src={decodedUrl} alt="生成的变体" className="w-full h-full object-cover" />;
/> } else if (!blobUrl.startsWith('blob:')) {
</> return <img src={blobUrl} alt="生成的变体" className="w-full h-full object-cover" />;
} else {
return (
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-yellow-400" />
</div>
);
}
})()
) : ( ) : (
<div className="w-full h-full bg-gray-800 flex items-center justify-center"> <div className="w-full h-full bg-gray-100 flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-yellow-400" /> <ImageIcon className="h-8 w-8 text-gray-400" />
</div> </div>
)} )}
{/* 变体编号 */} {/* 变体编号 */}
<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 text-white">
#{index + 1} G{index + 1}
</div> </div>
</div> </div>
))} ))}
{/* 显示编辑记录 */} {/* 显示编辑记录 */}
{edits.slice(-2).map((edit, index) => ( {[...edits].sort((a, b) => b.timestamp - a.timestamp).slice(0, 10).map((edit, index) => (
<div <div
key={edit.id} key={edit.id}
className={cn( className={cn(
'relative aspect-square rounded-lg border-2 cursor-pointer transition-all duration-200 overflow-hidden', 'relative aspect-square rounded-lg border-2 cursor-pointer transition-all duration-200 overflow-hidden',
selectedEditId === edit.id selectedEditId === edit.id
? 'border-yellow-400' ? 'border-purple-400'
: 'border-gray-700 hover:border-gray-600' : 'border-gray-300 hover:border-gray-400'
)} )}
onClick={() => { onClick={() => {
if (edit.outputAssets[0]) { selectEdit(edit.id);
setCanvasImage(edit.outputAssets[0].url); selectGeneration(null);
selectEdit(edit.id); // 设置画布图像为第一个输出资产
selectGeneration(null); if (edit.outputAssetsBlobUrls && edit.outputAssetsBlobUrls.length > 0) {
const blobUrl = edit.outputAssetsBlobUrls[0];
const decodedUrl = decodedImages[blobUrl];
if (decodedUrl) {
setCanvasImage(decodedUrl);
} else if (!blobUrl.startsWith('blob:')) {
// 如果不是Blob URL直接使用
setCanvasImage(blobUrl);
}
} }
}} }}
> >
{edit.outputAssets[0] ? ( {edit.outputAssetsBlobUrls && edit.outputAssetsBlobUrls.length > 0 ? (
<img (() => {
src={edit.outputAssets[0].url} const blobUrl = edit.outputAssetsBlobUrls[0];
alt="编辑的变体" const decodedUrl = decodedImages[blobUrl];
className="w-full h-full object-cover" if (decodedUrl) {
/> return <img src={decodedUrl} alt="编辑的变体" className="w-full h-full object-cover" />;
} else if (!blobUrl.startsWith('blob:')) {
return <img src={blobUrl} alt="编辑的变体" className="w-full h-full object-cover" />;
} else {
return (
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-purple-400" />
</div>
);
}
})()
) : ( ) : (
<div className="w-full h-full bg-gray-800 flex items-center justify-center"> <div className="w-full h-full bg-gray-100 flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-yellow-400" /> <ImageIcon className="h-8 w-8 text-gray-400" />
</div> </div>
)} )}
{/* 编辑标签 */} {/* 编辑标签 */}
<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 text-white">
#{index + 1} E{index + 1}
</div> </div>
</div> </div>
))} ))}
@@ -176,26 +271,26 @@ export const HistoryPanel: React.FC = () => {
{/* 当前图像信息 */} {/* 当前图像信息 */}
{(canvasImage || imageDimensions) && ( {(canvasImage || imageDimensions) && (
<div className="mb-4 p-3 bg-gray-100 rounded-lg border border-gray-300"> <div className="mb-4 p-3 bg-gray-50 rounded-lg border border-gray-200">
<h4 className="text-xs font-medium text-gray-400 mb-2"></h4> <h4 className="text-xs font-medium text-gray-500 mb-2"></h4>
<div className="space-y-1 text-xs text-gray-500"> <div className="space-y-1 text-xs text-gray-600">
{imageDimensions && ( {imageDimensions && (
<div className="flex justify-between"> <div className="flex justify-between">
<span>:</span> <span>:</span>
<span className="text-gray-300">{imageDimensions.width} × {imageDimensions.height}</span> <span className="text-gray-800">{imageDimensions.width} × {imageDimensions.height}</span>
</div> </div>
)} )}
<div className="flex justify-between"> <div className="flex justify-between">
<span>:</span> <span>:</span>
<span className="text-gray-300 capitalize">{selectedTool}</span> <span className="text-gray-800 capitalize">{selectedTool}</span>
</div> </div>
</div> </div>
</div> </div>
)} )}
{/* 生成详情 */} {/* 生成详情 */}
<div className="mb-6 p-4 bg-gray-100 rounded-lg border border-gray-300 flex-1 overflow-y-auto min-h-0"> <div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200 flex-1 overflow-y-auto min-h-0">
<h4 className="text-xs font-medium text-gray-400 mb-2"></h4> <h4 className="text-xs font-medium text-gray-500 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);
@@ -203,10 +298,10 @@ export const HistoryPanel: React.FC = () => {
if (gen) { if (gen) {
return ( return (
<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-600">
<div> <div>
<span className="text-gray-400">:</span> <span className="text-gray-500">:</span>
<p className="text-gray-300 mt-1">{gen.prompt}</p> <p className="text-gray-800 mt-1">{gen.prompt}</p>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>:</span> <span>:</span>
@@ -218,37 +313,18 @@ export const HistoryPanel: React.FC = () => {
<span>{gen.parameters.seed}</span> <span>{gen.parameters.seed}</span>
</div> </div>
)} )}
<div className="flex justify-between">
<span>:</span>
<span>{new Date(gen.timestamp).toLocaleString()}</span>
</div>
</div> </div>
{/* 参考图像 */} {/* 参考图像信息 */}
{gen.sourceAssets.length > 0 && ( {gen.sourceAssets.length > 0 && (
<div> <div>
<h5 className="text-xs font-medium text-gray-400 mb-2"></h5> <h5 className="text-xs font-medium text-gray-500 mb-2"></h5>
<div className="grid grid-cols-2 gap-2"> <div className="text-xs text-gray-600">
{gen.sourceAssets.map((asset, index) => ( {gen.sourceAssets.length}
<button
key={asset.id}
onClick={() => setPreviewModal({
open: true,
imageUrl: asset.url,
title: `参考图像 ${index + 1}`,
description: '此参考图像用于指导生成'
})}
className="relative aspect-square rounded border border-gray-700 hover:border-gray-600 transition-colors overflow-hidden group"
>
<img
src={asset.url}
alt={`参考 ${index + 1}`}
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">
<ImageIcon className="h-4 w-4 text-white opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
<div className="absolute bottom-1 left-1 bg-gray-900/80 text-xs px-1 py-0.5 rounded text-gray-300">
{index + 1}
</div>
</button>
))}
</div> </div>
</div> </div>
)} )}
@@ -258,10 +334,10 @@ export const HistoryPanel: React.FC = () => {
const parentGen = generations.find(g => g.id === selectedEdit.parentGenerationId); const parentGen = generations.find(g => g.id === selectedEdit.parentGenerationId);
return ( return (
<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-600">
<div> <div>
<span className="text-gray-400">:</span> <span className="text-gray-500">:</span>
<p className="text-gray-300 mt-1">{selectedEdit.instruction}</p> <p className="text-gray-800 mt-1">{selectedEdit.instruction}</p>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>:</span> <span>:</span>
@@ -269,12 +345,12 @@ export const HistoryPanel: React.FC = () => {
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>:</span> <span>:</span>
<span>{new Date(selectedEdit.timestamp).toLocaleTimeString()}</span> <span>{new Date(selectedEdit.timestamp).toLocaleString()}</span>
</div> </div>
{selectedEdit.maskAssetId && ( {selectedEdit.maskAssetId && (
<div className="flex justify-between"> <div className="flex justify-between">
<span>:</span> <span>:</span>
<span className="text-purple-400"></span> <span className="text-purple-600"></span>
</div> </div>
)} )}
</div> </div>
@@ -282,53 +358,10 @@ export const HistoryPanel: React.FC = () => {
{/* 原始生成参考 */} {/* 原始生成参考 */}
{parentGen && ( {parentGen && (
<div> <div>
<h5 className="text-xs font-medium text-gray-400 mb-2"></h5> <h5 className="text-xs font-medium text-gray-500 mb-2"></h5>
<button <div className="text-xs text-gray-600">
onClick={() => setPreviewModal({ 基于: G{generations.length - generations.indexOf(parentGen)}
open: true, </div>
imageUrl: parentGen.outputAssets[0]?.url || '',
title: '原始图像',
description: '被编辑的基础图像'
})}
className="relative aspect-square w-16 rounded border border-gray-700 hover:border-gray-600 transition-colors overflow-hidden group"
>
<img
src={parentGen.outputAssets[0]?.url}
alt="原始"
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">
<ImageIcon className="h-3 w-3 text-white opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</button>
</div>
)}
{/* 遮罩可视化 */}
{selectedEdit.maskReferenceAsset && (
<div>
<h5 className="text-xs font-medium text-gray-400 mb-2"></h5>
<button
onClick={() => setPreviewModal({
open: true,
imageUrl: selectedEdit.maskReferenceAsset!.url,
title: '遮罩参考图像',
description: '带有遮罩叠加的图像被发送到AI模型以指导编辑'
})}
className="relative aspect-square w-16 rounded border border-gray-700 hover:border-gray-600 transition-colors overflow-hidden group"
>
<img
src={selectedEdit.maskReferenceAsset.url}
alt="遮罩参考"
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">
<ImageIcon className="h-3 w-3 text-white opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
<div className="absolute bottom-1 left-1 bg-purple-900/80 text-xs px-1 py-0.5 rounded text-purple-300">
</div>
</button>
</div> </div>
)} )}
</div> </div>
@@ -336,7 +369,7 @@ 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"></p> <p className="text-gray-500"></p>
</div> </div>
); );
} }
@@ -354,8 +387,8 @@ export const HistoryPanel: React.FC = () => {
let imageUrl: string | null = null; let imageUrl: string | null = null;
if (selectedGenerationId) { if (selectedGenerationId) {
const gen = generations.find(g => g.id === selectedGenerationId); const { canvasImage } = useAppStore.getState();
imageUrl = gen?.outputAssets[0]?.url || null; imageUrl = canvasImage;
} else { } else {
// 如果没有选择生成记录,尝试获取当前画布图像 // 如果没有选择生成记录,尝试获取当前画布图像
const { canvasImage } = useAppStore.getState(); const { canvasImage } = useAppStore.getState();

View File

@@ -6,7 +6,7 @@ import { Generation, Edit, Asset } from '../types';
import { useToast } from '../components/ToastContext'; import { useToast } from '../components/ToastContext';
export const useImageGeneration = () => { export const useImageGeneration = () => {
const { addGeneration, setIsGenerating, setCanvasImage, setCurrentProject, currentProject } = useAppStore(); const { addGeneration, setIsGenerating, setCanvasImage } = useAppStore();
const { addToast } = useToast(); const { addToast } = useToast();
const generateMutation = useMutation({ const generateMutation = useMutation({
@@ -81,7 +81,6 @@ export const useImageEditing = () => {
editReferenceImages, editReferenceImages,
brushStrokes, brushStrokes,
selectedGenerationId, selectedGenerationId,
currentProject,
seed, seed,
temperature, temperature,
uploadedImages uploadedImages
@@ -229,7 +228,7 @@ export const useImageEditing = () => {
const edit: Edit = { const edit: Edit = {
id: generateId(), id: generateId(),
parentGenerationId: selectedGenerationId || (currentProject?.generations[currentProject.generations.length - 1]?.id || ''), parentGenerationId: selectedGenerationId || '',
maskAssetId: brushStrokes.length > 0 ? generateId() : undefined, maskAssetId: brushStrokes.length > 0 ? generateId() : undefined,
maskReferenceAsset, maskReferenceAsset,
instruction, instruction,

View File

@@ -1,11 +1,49 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { devtools } from 'zustand/middleware'; import { devtools, persist } from 'zustand/middleware';
import { Project, Generation, Edit, SegmentationMask, BrushStroke } from '../types'; import { Project, Generation, Edit, SegmentationMask, BrushStroke } from '../types';
import { generateId } from '../utils/imageUtils'; import { generateId } from '../utils/imageUtils';
// 定义不包含图像数据的轻量级项目结构
interface LightweightProject {
id: string;
title: string;
generations: Array<{
id: string;
prompt: string;
parameters: Generation['parameters'];
sourceAssets: Array<{
id: string;
type: 'original';
mime: string;
width: number;
height: number;
checksum: string;
// 存储Blob URL而不是base64数据
blobUrl: string;
}>;
// 存储输出资产的Blob URL
outputAssetsBlobUrls: string[];
modelVersion: string;
timestamp: number;
}>;
edits: Array<{
id: string;
parentGenerationId: string;
maskAssetId?: string;
// 存储遮罩参考资产的Blob URL
maskReferenceAssetBlobUrl?: string;
instruction: string;
// 存储输出资产的Blob URL
outputAssetsBlobUrls: string[];
timestamp: number;
}>;
createdAt: number;
updatedAt: number;
}
interface AppState { interface AppState {
// 当前项目 // 当前项目(轻量级版本,不包含实际图像数据)
currentProject: Project | null; currentProject: LightweightProject | null;
// 画布状态 // 画布状态
canvasImage: string | null; canvasImage: string | null;
@@ -38,8 +76,11 @@ interface AppState {
// UI状态 // UI状态
selectedTool: 'generate' | 'edit' | 'mask'; selectedTool: 'generate' | 'edit' | 'mask';
// 存储Blob对象的Map
blobStore: Map<string, Blob>;
// 操作 // 操作
setCurrentProject: (project: Project | null) => void; setCurrentProject: (project: LightweightProject | null) => void;
setCanvasImage: (url: string | null) => void; setCanvasImage: (url: string | null) => void;
setCanvasZoom: (zoom: number) => void; setCanvasZoom: (zoom: number) => void;
setCanvasPan: (pan: { x: number; y: number }) => void; setCanvasPan: (pan: { x: number; y: number }) => void;
@@ -71,103 +112,325 @@ interface AppState {
setShowPromptPanel: (show: boolean) => void; setShowPromptPanel: (show: boolean) => void;
setSelectedTool: (tool: 'generate' | 'edit' | 'mask') => void; setSelectedTool: (tool: 'generate' | 'edit' | 'mask') => void;
// Blob存储操作
addBlob: (blob: Blob) => string;
getBlob: (url: string) => Blob | undefined;
cleanupOldHistory: () => void;
} }
export const useAppStore = create<AppState>()( export const useAppStore = create<AppState>()(
devtools( devtools(
(set, get) => ({ persist(
// 初始状态 (set, get) => ({
currentProject: null, // 初始状态
canvasImage: null, currentProject: null,
canvasZoom: 1, canvasImage: null,
canvasPan: { x: 0, y: 0 }, canvasZoom: 1,
canvasPan: { x: 0, y: 0 },
uploadedImages: [], uploadedImages: [],
editReferenceImages: [], editReferenceImages: [],
brushStrokes: [], brushStrokes: [],
brushSize: 20, brushSize: 20,
showMasks: true, showMasks: true,
isGenerating: false, isGenerating: false,
currentPrompt: '', currentPrompt: '',
temperature: 0.7, temperature: 0.7,
seed: null, seed: null,
selectedGenerationId: null, selectedGenerationId: null,
selectedEditId: null, selectedEditId: null,
showHistory: true, showHistory: true,
showPromptPanel: true, showPromptPanel: true,
selectedTool: 'generate', selectedTool: 'generate',
// 操作 // Blob存储不在持久化中保存
setCurrentProject: (project) => set({ currentProject: project }), blobStore: new Map(),
setCanvasImage: (url) => set({ canvasImage: url }),
setCanvasZoom: (zoom) => set({ canvasZoom: zoom }),
setCanvasPan: (pan) => set({ canvasPan: pan }),
addUploadedImage: (url) => set((state) => ({ // 操作
uploadedImages: [...state.uploadedImages, url] setCurrentProject: (project) => set({ currentProject: project }),
})), setCanvasImage: (url) => set({ canvasImage: url }),
removeUploadedImage: (index) => set((state) => ({ setCanvasZoom: (zoom) => set({ canvasZoom: zoom }),
uploadedImages: state.uploadedImages.filter((_, i) => i !== index) setCanvasPan: (pan) => set({ canvasPan: pan }),
})),
clearUploadedImages: () => set({ uploadedImages: [] }),
addEditReferenceImage: (url) => set((state) => ({ addUploadedImage: (url) => set((state) => ({
editReferenceImages: [...state.editReferenceImages, url] uploadedImages: [...state.uploadedImages, url]
})), })),
removeEditReferenceImage: (index) => set((state) => ({ removeUploadedImage: (index) => set((state) => ({
editReferenceImages: state.editReferenceImages.filter((_, i) => i !== index) uploadedImages: state.uploadedImages.filter((_, i) => i !== index)
})), })),
clearEditReferenceImages: () => set({ editReferenceImages: [] }), clearUploadedImages: () => set({ uploadedImages: [] }),
addBrushStroke: (stroke) => set((state) => ({ addEditReferenceImage: (url) => set((state) => ({
brushStrokes: [...state.brushStrokes, stroke] editReferenceImages: [...state.editReferenceImages, url]
})), })),
clearBrushStrokes: () => set({ brushStrokes: [] }), removeEditReferenceImage: (index) => set((state) => ({
setBrushSize: (size) => set({ brushSize: size }), editReferenceImages: state.editReferenceImages.filter((_, i) => i !== index)
setShowMasks: (show) => set({ showMasks: show }), })),
clearEditReferenceImages: () => set({ editReferenceImages: [] }),
setIsGenerating: (generating) => set({ isGenerating: generating }), addBrushStroke: (stroke) => set((state) => ({
setCurrentPrompt: (prompt) => set({ currentPrompt: prompt }), brushStrokes: [...state.brushStrokes, stroke]
setTemperature: (temp) => set({ temperature: temp }), })),
setSeed: (seed) => set({ seed: seed }), clearBrushStrokes: () => set({ brushStrokes: [] }),
setBrushSize: (size) => set({ brushSize: size }),
setShowMasks: (show) => set({ showMasks: show }),
addGeneration: (generation) => set((state) => ({ setIsGenerating: (generating) => set({ isGenerating: generating }),
currentProject: state.currentProject ? { setCurrentPrompt: (prompt) => set({ currentPrompt: prompt }),
...state.currentProject, setTemperature: (temp) => set({ temperature: temp }),
generations: [...state.currentProject.generations, generation], setSeed: (seed) => set({ seed: seed }),
updatedAt: Date.now()
} : {
// 如果没有项目,创建一个新项目包含此生成记录
id: generateId(),
title: '未命名项目',
generations: [generation],
edits: [],
createdAt: Date.now(),
updatedAt: Date.now()
}
})),
addEdit: (edit) => set((state) => ({ // 添加Blob到存储并返回URL
currentProject: state.currentProject ? { addBlob: (blob: Blob) => {
...state.currentProject, const url = URL.createObjectURL(blob);
edits: [...state.currentProject.edits, edit], set((state) => {
updatedAt: Date.now() const newBlobStore = new Map(state.blobStore);
} : null newBlobStore.set(url, blob);
})), return { blobStore: newBlobStore };
});
return url;
},
selectGeneration: (id) => set({ selectedGenerationId: id }), // 从存储中获取Blob
selectEdit: (id) => set({ selectedEditId: id }), getBlob: (url: string) => {
setShowHistory: (show) => set({ showHistory: show }), return get().blobStore.get(url);
},
setShowPromptPanel: (show) => set({ showPromptPanel: show }), addGeneration: (generation) => set((state) => {
// 将base64图像数据转换为Blob并存储
const sourceAssets = generation.sourceAssets.map(asset => {
if (asset.url.startsWith('data:')) {
// 从base64创建Blob
const base64 = asset.url.split(',')[1];
const byteString = atob(base64);
const mimeString = asset.url.split(',')[0].split(':')[1].split(';')[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ab], { type: mimeString });
const blobUrl = URL.createObjectURL(blob);
setSelectedTool: (tool) => set({ selectedTool: tool }), // 存储Blob对象
}), set((innerState) => {
{ name: 'nano-banana-store' } const newBlobStore = new Map(innerState.blobStore);
newBlobStore.set(blobUrl, blob);
return { blobStore: newBlobStore };
});
return {
id: asset.id,
type: asset.type,
mime: asset.mime,
width: asset.width,
height: asset.height,
checksum: asset.checksum,
blobUrl
};
}
return {
id: asset.id,
type: asset.type,
mime: asset.mime,
width: asset.width,
height: asset.height,
checksum: asset.checksum,
blobUrl: asset.url
};
});
// 将输出资产转换为Blob URL
const outputAssetsBlobUrls = generation.outputAssets.map(asset => {
if (asset.url.startsWith('data:')) {
// 从base64创建Blob
const base64 = asset.url.split(',')[1];
const byteString = atob(base64);
const mimeString = asset.url.split(',')[0].split(':')[1].split(';')[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ab], { type: mimeString });
const blobUrl = URL.createObjectURL(blob);
// 存储Blob对象
set((innerState) => {
const newBlobStore = new Map(innerState.blobStore);
newBlobStore.set(blobUrl, blob);
return { blobStore: newBlobStore };
});
return blobUrl;
}
return asset.url;
});
// 创建轻量级生成记录
const lightweightGeneration = {
id: generation.id,
prompt: generation.prompt,
parameters: generation.parameters,
sourceAssets,
outputAssetsBlobUrls,
modelVersion: generation.modelVersion,
timestamp: generation.timestamp
};
const updatedProject = state.currentProject ? {
...state.currentProject,
generations: [...state.currentProject.generations, lightweightGeneration],
updatedAt: Date.now()
} : {
// 如果没有项目,创建一个新项目包含此生成记录
id: generateId(),
title: '未命名项目',
generations: [lightweightGeneration],
edits: [],
createdAt: Date.now(),
updatedAt: Date.now()
};
// 清理旧记录以保持在限制内
if (updatedProject.generations.length > 10) {
updatedProject.generations.splice(0, updatedProject.generations.length - 10);
}
return {
currentProject: updatedProject
};
}),
addEdit: (edit) => set((state) => {
// 将遮罩参考资产转换为Blob URL如果存在
let maskReferenceAssetBlobUrl: string | undefined;
if (edit.maskReferenceAsset && edit.maskReferenceAsset.url.startsWith('data:')) {
const base64 = edit.maskReferenceAsset.url.split(',')[1];
const byteString = atob(base64);
const mimeString = edit.maskReferenceAsset.url.split(',')[0].split(':')[1].split(';')[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ab], { type: mimeString });
maskReferenceAssetBlobUrl = URL.createObjectURL(blob);
// 存储Blob对象
set((innerState) => {
const newBlobStore = new Map(innerState.blobStore);
newBlobStore.set(maskReferenceAssetBlobUrl!, blob);
return { blobStore: newBlobStore };
});
} else if (edit.maskReferenceAsset) {
maskReferenceAssetBlobUrl = edit.maskReferenceAsset.url;
}
// 将输出资产转换为Blob URL
const outputAssetsBlobUrls = edit.outputAssets.map(asset => {
if (asset.url.startsWith('data:')) {
// 从base64创建Blob
const base64 = asset.url.split(',')[1];
const byteString = atob(base64);
const mimeString = asset.url.split(',')[0].split(':')[1].split(';')[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ab], { type: mimeString });
const blobUrl = URL.createObjectURL(blob);
// 存储Blob对象
set((innerState) => {
const newBlobStore = new Map(innerState.blobStore);
newBlobStore.set(blobUrl, blob);
return { blobStore: newBlobStore };
});
return blobUrl;
}
return asset.url;
});
// 创建轻量级编辑记录
const lightweightEdit = {
id: edit.id,
parentGenerationId: edit.parentGenerationId,
maskAssetId: edit.maskAssetId,
maskReferenceAssetBlobUrl,
instruction: edit.instruction,
outputAssetsBlobUrls,
timestamp: edit.timestamp
};
if (!state.currentProject) return {};
const updatedProject = {
...state.currentProject,
edits: [...state.currentProject.edits, lightweightEdit],
updatedAt: Date.now()
};
// 清理旧记录以保持在限制内
if (updatedProject.edits.length > 10) {
updatedProject.edits.splice(0, updatedProject.edits.length - 10);
}
return {
currentProject: updatedProject
};
}),
selectGeneration: (id) => set({ selectedGenerationId: id }),
selectEdit: (id) => set({ selectedEditId: id }),
setShowHistory: (show) => set({ showHistory: show }),
setShowPromptPanel: (show) => set({ showPromptPanel: show }),
setSelectedTool: (tool) => set({ selectedTool: tool }),
// 清理旧的历史记录保留最多10条
cleanupOldHistory: () => set((state) => {
if (!state.currentProject) return {};
const generations = [...state.currentProject.generations];
const edits = [...state.currentProject.edits];
// 如果生成记录超过10条只保留最新的10条
if (generations.length > 10) {
generations.splice(0, generations.length - 10);
}
// 如果编辑记录超过10条只保留最新的10条
if (edits.length > 10) {
edits.splice(0, edits.length - 10);
}
return {
currentProject: {
...state.currentProject,
generations,
edits,
updatedAt: Date.now()
}
};
})
}),
{
name: 'nano-banana-store',
partialize: (state) => ({
currentProject: state.currentProject,
// 我们只持久化轻量级项目数据不包含Blob对象
})
}
)
) )
); );