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

新增 增加最大历史记录条数为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 { 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 { ImagePreviewModal } from './ImagePreviewModal';
@@ -19,6 +19,8 @@ export const HistoryPanel: React.FC = () => {
selectedTool
} = useAppStore();
const { getBlob } = useAppStore.getState();
const [previewModal, setPreviewModal] = React.useState<{
open: boolean;
imageUrl: string;
@@ -31,6 +33,9 @@ export const HistoryPanel: React.FC = () => {
description: ''
});
// 存储从Blob URL解码的图像数据
const [decodedImages, setDecodedImages] = useState<Record<string, string>>({});
const generations = currentProject?.generations || [];
const edits = currentProject?.edits || [];
@@ -49,6 +54,57 @@ export const HistoryPanel: React.FC = () => {
}
}, [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) {
return (
<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">
<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 ? (
<div className="text-center py-8">
<div className="text-4xl mb-2">🖼</div>
<p className="text-sm text-gray-500"></p>
</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
key={generation.id}
className={cn(
'relative aspect-square rounded-lg border-2 cursor-pointer transition-all duration-200 overflow-hidden',
selectedGenerationId === generation.id
? 'border-yellow-400'
: 'border-gray-700 hover:border-gray-600'
: 'border-gray-300 hover:border-gray-400'
)}
onClick={() => {
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] ? (
<>
<img
src={generation.outputAssets[0].url}
alt="生成的变体"
className="w-full h-full object-cover"
/>
</>
) : (
<div className="w-full h-full bg-gray-800 flex items-center justify-center">
{generation.outputAssetsBlobUrls && generation.outputAssetsBlobUrls.length > 0 ? (
(() => {
const blobUrl = generation.outputAssetsBlobUrls[0];
const decodedUrl = decodedImages[blobUrl];
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-yellow-400" />
</div>
);
}
})()
) : (
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
<ImageIcon className="h-8 w-8 text-gray-400" />
</div>
)}
{/* 变体编号 */}
<div className="absolute top-2 left-2 bg-gray-900/80 text-xs px-2 py-1 rounded">
#{index + 1}
<div className="absolute top-2 left-2 bg-gray-900/80 text-xs px-2 py-1 rounded text-white">
G{index + 1}
</div>
</div>
))}
{/* 显示编辑记录 */}
{edits.slice(-2).map((edit, index) => (
{[...edits].sort((a, b) => b.timestamp - a.timestamp).slice(0, 10).map((edit, index) => (
<div
key={edit.id}
className={cn(
'relative aspect-square rounded-lg border-2 cursor-pointer transition-all duration-200 overflow-hidden',
selectedEditId === edit.id
? 'border-yellow-400'
: 'border-gray-700 hover:border-gray-600'
? 'border-purple-400'
: 'border-gray-300 hover:border-gray-400'
)}
onClick={() => {
if (edit.outputAssets[0]) {
setCanvasImage(edit.outputAssets[0].url);
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] ? (
<img
src={edit.outputAssets[0].url}
alt="编辑的变体"
className="w-full h-full object-cover"
/>
{edit.outputAssetsBlobUrls && edit.outputAssetsBlobUrls.length > 0 ? (
(() => {
const blobUrl = edit.outputAssetsBlobUrls[0];
const decodedUrl = decodedImages[blobUrl];
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="animate-spin rounded-full h-6 w-6 border-b-2 border-yellow-400" />
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
<ImageIcon className="h-8 w-8 text-gray-400" />
</div>
)}
{/* 编辑标签 */}
<div className="absolute top-2 left-2 bg-purple-900/80 text-xs px-2 py-1 rounded">
#{index + 1}
<div className="absolute top-2 left-2 bg-purple-900/80 text-xs px-2 py-1 rounded text-white">
E{index + 1}
</div>
</div>
))}
@@ -176,26 +271,26 @@ export const HistoryPanel: React.FC = () => {
{/* 当前图像信息 */}
{(canvasImage || imageDimensions) && (
<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"></h4>
<div className="space-y-1 text-xs text-gray-500">
<div className="mb-4 p-3 bg-gray-50 rounded-lg border border-gray-200">
<h4 className="text-xs font-medium text-gray-500 mb-2"></h4>
<div className="space-y-1 text-xs text-gray-600">
{imageDimensions && (
<div className="flex justify-between">
<span>:</span>
<span className="text-gray-300">{imageDimensions.width} × {imageDimensions.height}</span>
<span className="text-gray-800">{imageDimensions.width} × {imageDimensions.height}</span>
</div>
)}
<div className="flex justify-between">
<span>:</span>
<span className="text-gray-300 capitalize">{selectedTool}</span>
<span className="text-gray-800 capitalize">{selectedTool}</span>
</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">
<h4 className="text-xs font-medium text-gray-400 mb-2"></h4>
<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-500 mb-2"></h4>
{(() => {
const gen = generations.find(g => g.id === selectedGenerationId);
const selectedEdit = edits.find(e => e.id === selectedEditId);
@@ -203,10 +298,10 @@ export const HistoryPanel: React.FC = () => {
if (gen) {
return (
<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>
<span className="text-gray-400">:</span>
<p className="text-gray-300 mt-1">{gen.prompt}</p>
<span className="text-gray-500">:</span>
<p className="text-gray-800 mt-1">{gen.prompt}</p>
</div>
<div className="flex justify-between">
<span>:</span>
@@ -218,37 +313,18 @@ export const HistoryPanel: React.FC = () => {
<span>{gen.parameters.seed}</span>
</div>
)}
<div className="flex justify-between">
<span>:</span>
<span>{new Date(gen.timestamp).toLocaleString()}</span>
</div>
</div>
{/* 参考图像 */}
{/* 参考图像信息 */}
{gen.sourceAssets.length > 0 && (
<div>
<h5 className="text-xs font-medium text-gray-400 mb-2"></h5>
<div className="grid grid-cols-2 gap-2">
{gen.sourceAssets.map((asset, index) => (
<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>
))}
<h5 className="text-xs font-medium text-gray-500 mb-2"></h5>
<div className="text-xs text-gray-600">
{gen.sourceAssets.length}
</div>
</div>
)}
@@ -258,10 +334,10 @@ export const HistoryPanel: React.FC = () => {
const parentGen = generations.find(g => g.id === selectedEdit.parentGenerationId);
return (
<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>
<span className="text-gray-400">:</span>
<p className="text-gray-300 mt-1">{selectedEdit.instruction}</p>
<span className="text-gray-500">:</span>
<p className="text-gray-800 mt-1">{selectedEdit.instruction}</p>
</div>
<div className="flex justify-between">
<span>:</span>
@@ -269,12 +345,12 @@ export const HistoryPanel: React.FC = () => {
</div>
<div className="flex justify-between">
<span>:</span>
<span>{new Date(selectedEdit.timestamp).toLocaleTimeString()}</span>
<span>{new Date(selectedEdit.timestamp).toLocaleString()}</span>
</div>
{selectedEdit.maskAssetId && (
<div className="flex justify-between">
<span>:</span>
<span className="text-purple-400"></span>
<span className="text-purple-600"></span>
</div>
)}
</div>
@@ -282,53 +358,10 @@ export const HistoryPanel: React.FC = () => {
{/* 原始生成参考 */}
{parentGen && (
<div>
<h5 className="text-xs font-medium text-gray-400 mb-2"></h5>
<button
onClick={() => setPreviewModal({
open: true,
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" />
<h5 className="text-xs font-medium text-gray-500 mb-2"></h5>
<div className="text-xs text-gray-600">
基于: G{generations.length - generations.indexOf(parentGen)}
</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>
@@ -336,7 +369,7 @@ export const HistoryPanel: React.FC = () => {
} else {
return (
<div className="space-y-2 text-xs text-gray-500">
<p className="text-gray-400"></p>
<p className="text-gray-500"></p>
</div>
);
}
@@ -354,8 +387,8 @@ export const HistoryPanel: React.FC = () => {
let imageUrl: string | null = null;
if (selectedGenerationId) {
const gen = generations.find(g => g.id === selectedGenerationId);
imageUrl = gen?.outputAssets[0]?.url || null;
const { canvasImage } = useAppStore.getState();
imageUrl = canvasImage;
} else {
// 如果没有选择生成记录,尝试获取当前画布图像
const { canvasImage } = useAppStore.getState();

View File

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

View File

@@ -1,11 +1,49 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { devtools, persist } from 'zustand/middleware';
import { Project, Generation, Edit, SegmentationMask, BrushStroke } from '../types';
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 {
// 当前项目
currentProject: Project | null;
// 当前项目(轻量级版本,不包含实际图像数据)
currentProject: LightweightProject | null;
// 画布状态
canvasImage: string | null;
@@ -38,8 +76,11 @@ interface AppState {
// UI状态
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;
setCanvasZoom: (zoom: number) => void;
setCanvasPan: (pan: { x: number; y: number }) => void;
@@ -71,10 +112,16 @@ interface AppState {
setShowPromptPanel: (show: boolean) => void;
setSelectedTool: (tool: 'generate' | 'edit' | 'mask') => void;
// Blob存储操作
addBlob: (blob: Blob) => string;
getBlob: (url: string) => Blob | undefined;
cleanupOldHistory: () => void;
}
export const useAppStore = create<AppState>()(
devtools(
persist(
(set, get) => ({
// 初始状态
currentProject: null,
@@ -102,6 +149,9 @@ export const useAppStore = create<AppState>()(
selectedTool: 'generate',
// Blob存储不在持久化中保存
blobStore: new Map(),
// 操作
setCurrentProject: (project) => set({ currentProject: project }),
setCanvasImage: (url) => set({ canvasImage: url }),
@@ -136,29 +186,208 @@ export const useAppStore = create<AppState>()(
setTemperature: (temp) => set({ temperature: temp }),
setSeed: (seed) => set({ seed: seed }),
addGeneration: (generation) => set((state) => ({
currentProject: state.currentProject ? {
// 添加Blob到存储并返回URL
addBlob: (blob: Blob) => {
const url = URL.createObjectURL(blob);
set((state) => {
const newBlobStore = new Map(state.blobStore);
newBlobStore.set(url, blob);
return { blobStore: newBlobStore };
});
return url;
},
// 从存储中获取Blob
getBlob: (url: string) => {
return get().blobStore.get(url);
},
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);
// 存储Blob对象
set((innerState) => {
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, generation],
generations: [...state.currentProject.generations, lightweightGeneration],
updatedAt: Date.now()
} : {
// 如果没有项目,创建一个新项目包含此生成记录
id: generateId(),
title: '未命名项目',
generations: [generation],
generations: [lightweightGeneration],
edits: [],
createdAt: Date.now(),
updatedAt: Date.now()
}
})),
};
addEdit: (edit) => set((state) => ({
currentProject: state.currentProject ? {
// 清理旧记录以保持在限制内
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, edit],
edits: [...state.currentProject.edits, lightweightEdit],
updatedAt: Date.now()
} : null
})),
};
// 清理旧记录以保持在限制内
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 }),
@@ -167,7 +396,41 @@ export const useAppStore = create<AppState>()(
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' }
{
name: 'nano-banana-store',
partialize: (state) => ({
currentProject: state.currentProject,
// 我们只持久化轻量级项目数据不包含Blob对象
})
}
)
)
);