Files
Nano-Banana-AI-Image-Editor/src/components/HistoryPanel.tsx
2025-09-16 13:05:57 +08:00

815 lines
33 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { useAppStore } from '../store/useAppStore';
import { Button } from './ui/Button';
import { History, Download, Image as ImageIcon } from 'lucide-react';
import { cn } from '../utils/cn';
import { ImagePreviewModal } from './ImagePreviewModal';
import * as indexedDBService from '../services/indexedDBService';
import { useIndexedDBListener } from '../hooks/useIndexedDBListener';
export const HistoryPanel: React.FC = () => {
const {
currentProject,
canvasImage,
selectedGenerationId,
selectedEditId,
selectGeneration,
selectEdit,
showHistory,
setShowHistory,
setCanvasImage,
selectedTool
} = useAppStore();
const { getBlob } = useAppStore.getState();
const [previewModal, setPreviewModal] = React.useState<{
open: boolean;
imageUrl: string;
title: string;
description?: string;
}>({
open: false,
imageUrl: '',
title: '',
description: ''
});
// 存储从Blob URL解码的图像数据
const [decodedImages, setDecodedImages] = useState<Record<string, string>>({});
// 使用自定义hook获取IndexedDB记录
const { generations: dbGenerations, edits: dbEdits, loading, error, refresh } = useIndexedDBListener();
// 筛选和搜索状态
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [searchTerm, setSearchTerm] = useState<string>('');
// 悬浮预览状态
const [hoveredImage, setHoveredImage] = useState<{url: string, title: string} | null>(null);
const [previewPosition, setPreviewPosition] = useState<{x: number, y: number}>({x: 0, y: 0});
const generations = currentProject?.generations || [];
const edits = currentProject?.edits || [];
// 获取上传后的图片链接
const getUploadedImageUrl = (generationOrEdit: any, index: number) => {
if (generationOrEdit.uploadResults && generationOrEdit.uploadResults[index]) {
const uploadResult = generationOrEdit.uploadResults[index];
if (uploadResult.success && uploadResult.url) {
// 添加参数以降低图片质量
return `${uploadResult.url}?x-oss-process=image/quality,q_50`;
}
}
return null;
};
// 获取当前图像尺寸
const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null);
React.useEffect(() => {
if (canvasImage) {
const img = new Image();
img.onload = () => {
setImageDimensions({ width: img.width, height: img.height });
};
img.src = canvasImage;
} else {
setImageDimensions(null);
}
}, [canvasImage]);
// 错误处理显示
if (error) {
return (
<div className="w-80 bg-white border-l border-gray-200 p-6 flex flex-col h-full">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-2">
<History className="h-5 w-5 text-gray-400" />
<h3 className="text-sm font-medium text-gray-300"></h3>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setShowHistory(!showHistory)}
className="h-6 w-6 rounded-full card"
title="隐藏历史面板"
>
×
</Button>
</div>
<div className="text-center py-8 text-red-500">
<p className="text-sm">: {error}</p>
<Button
variant="outline"
size="sm"
className="mt-2 card"
onClick={refresh}
>
</Button>
</div>
</div>
);
}
// 筛选记录的函数
const filterRecords = (records: any[], isGeneration: boolean) => {
return records.filter(record => {
// 日期筛选
const recordDate = new Date(record.timestamp);
if (startDate && recordDate < new Date(startDate)) return false;
if (endDate && recordDate > new Date(endDate)) return false;
// 搜索词筛选
if (searchTerm) {
if (isGeneration) {
// 生成记录按提示词搜索
return record.prompt.toLowerCase().includes(searchTerm.toLowerCase());
} else {
// 编辑记录按指令搜索
return record.instruction.toLowerCase().includes(searchTerm.toLowerCase());
}
}
return true;
});
};
// 筛选后的记录
const filteredGenerations = filterRecords(dbGenerations, true);
const filteredEdits = filterRecords(dbEdits, false);
// 当项目变化时解码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 flex flex-col items-center justify-center rounded-r-xl">
<button
onClick={() => setShowHistory(true)}
className="w-6 h-16 bg-gray-100 hover:bg-gray-200 rounded-l-lg flex items-center justify-center transition-colors group"
title="显示历史面板"
>
<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>
<div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full"></div>
</div>
</button>
</div>
);
}
return (
<div className="w-72 bg-white p-4 flex flex-col h-full">
{/* 头部 */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
<History className="h-4 w-4 text-gray-500" />
<h3 className="text-sm font-medium text-gray-700"></h3>
</div>
<div className="flex space-x-1">
<Button
variant="ghost"
size="icon"
onClick={refresh}
className="h-6 w-6 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full card"
title="刷新历史记录"
>
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setShowHistory(!showHistory)}
className="h-6 w-6 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full card"
title="隐藏历史面板"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</Button>
</div>
</div>
{/* 筛选和搜索控件 */}
<div className="mb-3 space-y-2">
<div className="flex space-x-1">
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="flex-1 text-xs p-1.5 border border-gray-200 rounded-lg text-gray-600 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-yellow-400 card"
placeholder="开始日期"
/>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="flex-1 text-xs p-1.5 border border-gray-200 rounded-lg text-gray-600 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-yellow-400 card"
placeholder="结束日期"
/>
</div>
<div className="flex">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 text-xs p-1.5 border border-gray-200 rounded-l-lg bg-gray-50 focus:outline-none focus:ring-1 focus:ring-yellow-400 card"
placeholder="搜索提示词..."
/>
<Button
variant="outline"
size="sm"
className="text-xs p-1.5 rounded-l-none h-7 border-gray-200 text-gray-600 hover:bg-gray-100 card"
onClick={() => {
setStartDate('');
setEndDate('');
setSearchTerm('');
}}
>
</Button>
</div>
</div>
{/* 变体网格 */}
<div className="mb-6 flex-shrink-0">
<div className="flex items-center justify-between mb-2">
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide"></h4>
<span className="text-xs text-gray-400">
{filteredGenerations.length + filteredEdits.length}/1000
</span>
</div>
{filteredGenerations.length === 0 && filteredEdits.length === 0 ? (
<div className="text-center py-8">
<div className="text-2xl mb-2 text-gray-300">🖼</div>
<p className="text-xs text-gray-400"></p>
</div>
) : (
<div className="grid grid-cols-3 gap-1.5 max-h-72 overflow-y-auto">
{/* 显示生成记录 */}
{[...filteredGenerations].sort((a, b) => b.timestamp - a.timestamp).slice(0, 50).map((generation, index) => (
<div
key={generation.id}
className={cn(
'relative aspect-square rounded border cursor-pointer transition-all duration-200 overflow-hidden',
selectedGenerationId === generation.id
? 'border-yellow-400 ring-2 ring-yellow-400/30'
: 'border-gray-200 hover:border-gray-300'
)}
onClick={() => {
selectGeneration(generation.id);
// 设置画布图像为第一个输出资产
if (generation.outputAssets && generation.outputAssets.length > 0) {
const asset = generation.outputAssets[0];
if (asset.url) {
setCanvasImage(asset.url);
}
}
}}
onMouseEnter={(e) => {
if (generation.outputAssets && generation.outputAssets.length > 0) {
const asset = generation.outputAssets[0];
if (asset.url) {
setHoveredImage({
url: asset.url,
title: `生成记录 G${index + 1}`,
description: generation.prompt
});
setPreviewPosition({x: e.clientX, y: e.clientY});
}
}
}}
onMouseMove={(e) => {
// 调整预览位置以避免被遮挡
const previewWidth = 300;
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
let x = e.clientX + offsetX;
let y = e.clientY + offsetY;
// 检查是否超出右边界
if (x + previewWidth > window.innerWidth) {
x = window.innerWidth - previewWidth - 10;
}
// 检查是否超出下边界
if (y + previewHeight > window.innerHeight) {
y = window.innerHeight - previewHeight - 10;
}
// 检查是否超出左边界
if (x < 0) {
x = 10;
}
// 检查是否超出上边界
if (y < 0) {
y = 10;
}
setPreviewPosition({x, y});
}}
onMouseLeave={() => {
setHoveredImage(null);
}}
>
{generation.outputAssets && generation.outputAssets.length > 0 ? (
(() => {
const asset = generation.outputAssets[0];
if (asset.url) {
// 如果是base64数据URL直接显示
if (asset.url.startsWith('data:')) {
return <img src={asset.url} alt="生成的变体" className="w-full h-full object-cover" />;
}
// 如果是普通URL直接显示
return <img src={asset.url} alt="生成的变体" className="w-full h-full object-cover" />;
} else {
return (
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
<ImageIcon className="h-4 w-4 text-gray-400" />
</div>
);
}
})()
) : (
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
<ImageIcon className="h-4 w-4 text-gray-400" />
</div>
)}
{/* 变体编号 */}
<div className="absolute top-1 left-1 bg-gray-900/80 text-xs px-1 py-0.5 rounded text-white">
G{index + 1}
</div>
</div>
))}
{/* 显示编辑记录 */}
{[...filteredEdits].sort((a, b) => b.timestamp - a.timestamp).slice(0, 50).map((edit, index) => (
<div
key={edit.id}
className={cn(
'relative aspect-square rounded border cursor-pointer transition-all duration-200 overflow-hidden',
selectedEditId === edit.id
? 'border-purple-400 ring-2 ring-purple-400/30'
: 'border-gray-200 hover:border-gray-300'
)}
onClick={() => {
selectEdit(edit.id);
selectGeneration(null);
// 设置画布图像为第一个输出资产
if (edit.outputAssets && edit.outputAssets.length > 0) {
const asset = edit.outputAssets[0];
if (asset.url) {
setCanvasImage(asset.url);
}
}
}}
onMouseEnter={(e) => {
if (edit.outputAssets && edit.outputAssets.length > 0) {
const asset = edit.outputAssets[0];
if (asset.url) {
setHoveredImage({
url: asset.url,
title: `编辑记录 E${index + 1}`,
description: edit.instruction
});
setPreviewPosition({x: e.clientX, y: e.clientY});
}
}
}}
onMouseMove={(e) => {
// 调整预览位置以避免被遮挡
const previewWidth = 300;
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
let x = e.clientX + offsetX;
let y = e.clientY + offsetY;
// 检查是否超出右边界
if (x + previewWidth > window.innerWidth) {
x = window.innerWidth - previewWidth - 10;
}
// 检查是否超出下边界
if (y + previewHeight > window.innerHeight) {
y = window.innerHeight - previewHeight - 10;
}
// 检查是否超出左边界
if (x < 0) {
x = 10;
}
// 检查是否超出上边界
if (y < 0) {
y = 10;
}
setPreviewPosition({x, y});
}}
onMouseLeave={() => {
setHoveredImage(null);
}}
>
{edit.outputAssets && edit.outputAssets.length > 0 ? (
(() => {
const asset = edit.outputAssets[0];
if (asset.url) {
// 如果是base64数据URL直接显示
if (asset.url.startsWith('data:')) {
return <img src={asset.url} alt="编辑的变体" className="w-full h-full object-cover" />;
}
// 如果是普通URL直接显示
return <img src={asset.url} alt="编辑的变体" className="w-full h-full object-cover" />;
} else {
return (
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
<ImageIcon className="h-4 w-4 text-gray-400" />
</div>
);
}
})()
) : (
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
<ImageIcon className="h-4 w-4 text-gray-400" />
</div>
)}
{/* 编辑标签 */}
<div className="absolute top-1 left-1 bg-purple-900/80 text-xs px-1 py-0.5 rounded text-white">
E{index + 1}
</div>
</div>
))}
</div>
)}
</div>
{/* 生成详情 */}
<div className="flex-1 overflow-y-auto min-h-0">
<h4 className="text-xs font-medium text-gray-500 mb-2 uppercase tracking-wide"></h4>
{(() => {
const gen = filteredGenerations.find(g => g.id === selectedGenerationId) || dbGenerations.find(g => g.id === selectedGenerationId);
const selectedEdit = filteredEdits.find(e => e.id === selectedEditId) || dbEdits.find(e => e.id === selectedEditId);
if (gen) {
return (
<div className="space-y-3 p-3 bg-gray-50 rounded-lg">
<div className="space-y-2.5 text-xs text-gray-700">
<div>
<span className="text-gray-500">:</span>
<p className="text-gray-800 mt-1 leading-relaxed">{gen.prompt}</p>
</div>
<div className="flex justify-between">
<span className="text-gray-500">:</span>
<span className="text-gray-700">{gen.modelVersion}</span>
</div>
{gen.parameters.seed && (
<div className="flex justify-between">
<span className="text-gray-500">:</span>
<span className="text-gray-700 font-mono">{gen.parameters.seed}</span>
</div>
)}
<div className="flex justify-between">
<span className="text-gray-500">:</span>
<span className="text-gray-700">{new Date(gen.timestamp).toLocaleString()}</span>
</div>
</div>
{/* 上传结果 */}
{gen.uploadResults && gen.uploadResults.length > 0 && (
<div className="pt-2 border-t border-gray-200">
<h5 className="text-xs font-medium text-gray-500 mb-2"></h5>
<div className="space-y-1.5">
{gen.uploadResults.map((result, index) => (
<div key={index} className="text-xs">
<div className="flex justify-between">
<span className="text-gray-500"> {index + 1}:</span>
<span className={result.success ? 'text-green-600' : 'text-red-600'}>
{result.success ? '成功' : '失败'}
</span>
</div>
{result.success && result.url && (
<div className="text-blue-600 truncate text-xs mt-0.5">
{result.url.split('/').pop()}
</div>
)}
{result.error && (
<div className="text-red-600 truncate text-xs mt-0.5">
{result.error}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* 参考图像信息 */}
{gen.sourceAssets && gen.sourceAssets.length > 0 && (
<div className="pt-2 border-t border-gray-200">
<h5 className="text-xs font-medium text-gray-500 mb-2"></h5>
<div className="text-xs text-gray-600 mb-2">
{gen.sourceAssets.length}
</div>
<div className="flex flex-wrap gap-2">
{gen.sourceAssets.slice(0, 4).map((asset: any, index: number) => (
<div
key={asset.id}
className="relative w-16 h-16 rounded border overflow-hidden cursor-pointer hover:ring-2 hover:ring-yellow-400"
onClick={(e) => {
e.stopPropagation();
setPreviewModal({
open: true,
imageUrl: asset.url,
title: `参考图像 ${index + 1}`,
description: `${asset.width} × ${asset.height}`
});
}}
>
<img
src={asset.url}
alt={`参考图像 ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
))}
{gen.sourceAssets.length > 4 && (
<div className="w-16 h-16 rounded border flex items-center justify-center bg-gray-100 text-xs text-gray-500">
+{gen.sourceAssets.length - 4}
</div>
)}
</div>
</div>
)}
</div>
);
} else if (selectedEdit) {
const parentGen = filteredGenerations.find(g => g.id === selectedEdit.parentGenerationId) || dbGenerations.find(g => g.id === selectedEdit.parentGenerationId);
return (
<div className="space-y-3 p-3 bg-gray-50 rounded-lg">
<div className="space-y-2.5 text-xs text-gray-700">
<div>
<span className="text-gray-500">:</span>
<p className="text-gray-800 mt-1 leading-relaxed">{selectedEdit.instruction}</p>
</div>
<div className="flex justify-between">
<span className="text-gray-500">:</span>
<span className="text-gray-700"></span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">:</span>
<span className="text-gray-700">{new Date(selectedEdit.timestamp).toLocaleString()}</span>
</div>
{selectedEdit.maskAssetId && (
<div className="flex justify-between">
<span className="text-gray-500">:</span>
<span className="text-purple-600"></span>
</div>
)}
</div>
{/* 上传结果 */}
{selectedEdit.uploadResults && selectedEdit.uploadResults.length > 0 && (
<div className="pt-2 border-t border-gray-200">
<h5 className="text-xs font-medium text-gray-500 mb-2"></h5>
<div className="space-y-1.5">
{selectedEdit.uploadResults.map((result, index) => (
<div key={index} className="text-xs">
<div className="flex justify-between">
<span className="text-gray-500"> {index + 1}:</span>
<span className={result.success ? 'text-green-600' : 'text-red-600'}>
{result.success ? '成功' : '失败'}
</span>
</div>
{result.success && result.url && (
<div className="text-blue-600 truncate text-xs mt-0.5">
{result.url.split('/').pop()}
</div>
)}
{result.error && (
<div className="text-red-600 truncate text-xs mt-0.5">
{result.error}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* 原始生成参考 */}
{parentGen && (
<div className="pt-2 border-t border-gray-200">
<h5 className="text-xs font-medium text-gray-500 mb-2"></h5>
<div className="text-xs text-gray-600">
基于: G{dbGenerations.findIndex(g => g.id === parentGen.id) + 1}
</div>
{/* 显示原始生成的参考图像 */}
{parentGen.sourceAssets && parentGen.sourceAssets.length > 0 && (
<div className="mt-2">
<div className="text-xs text-gray-600 mb-2">
:
</div>
<div className="flex flex-wrap gap-2">
{parentGen.sourceAssets.slice(0, 4).map((asset: any, index: number) => (
<div
key={asset.id}
className="relative w-16 h-16 rounded border overflow-hidden cursor-pointer hover:ring-2 hover:ring-yellow-400"
onClick={(e) => {
e.stopPropagation();
setPreviewModal({
open: true,
imageUrl: asset.url,
title: `原始参考图像 ${index + 1}`,
description: `${asset.width} × ${asset.height}`
});
}}
>
<img
src={asset.url}
alt={`原始参考图像 ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
))}
{parentGen.sourceAssets.length > 4 && (
<div className="w-16 h-16 rounded border flex items-center justify-center bg-gray-100 text-xs text-gray-500">
+{parentGen.sourceAssets.length - 4}
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
);
} else {
return (
<div className="space-y-2 text-xs text-gray-500 p-3 text-center">
<p className="text-gray-400"></p>
</div>
);
}
})()}
</div>
{/* 操作 */}
<div className="space-y-2 flex-shrink-0 pt-2 border-t border-gray-100">
<Button
variant="outline"
size="sm"
className="w-full h-9 text-sm card"
onClick={() => {
// 查找当前显示的图像(生成记录或编辑记录)
let imageUrl: string | null = null;
if (selectedGenerationId) {
const { canvasImage } = useAppStore.getState();
imageUrl = canvasImage;
} else {
// 如果没有选择生成记录,尝试获取当前画布图像
const { canvasImage } = useAppStore.getState();
imageUrl = canvasImage;
}
if (imageUrl) {
// 处理数据URL和常规URL
if (imageUrl.startsWith('data:')) {
const link = document.createElement('a');
link.href = imageUrl;
link.download = `nano-banana-${Date.now()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
// 对于外部URL我们需要获取并转换为blob
fetch(imageUrl)
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `nano-banana-${Date.now()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
});
}
}
}}
disabled={!selectedGenerationId && !useAppStore.getState().canvasImage}
>
<Download className="h-4 w-4 mr-2" />
</Button>
</div>
{/* 图像预览模态框 */}
<ImagePreviewModal
open={previewModal.open}
onOpenChange={(open) => setPreviewModal(prev => ({ ...prev, open }))}
imageUrl={previewModal.imageUrl}
title={previewModal.title}
description={previewModal.description}
/>
{/* 悬浮预览 */}
{hoveredImage && (
<div
className="fixed z-50 shadow-2xl border border-gray-300 rounded-lg overflow-hidden bg-white backdrop-blur-sm"
style={{
left: Math.min(previewPosition.x + 10, window.innerWidth - 250),
top: Math.min(previewPosition.y + 10, window.innerHeight - 250),
maxWidth: '250px',
maxHeight: '250px'
}}
>
<div className="bg-gray-900 text-white text-xs p-2 truncate font-medium">
{hoveredImage.title}
</div>
<img
src={hoveredImage.url}
alt="预览"
className="w-full h-auto max-h-[150px] object-contain"
/>
{/* 图像信息 */}
<div className="p-2 bg-gray-50 border-t border-gray-200 text-xs">
{imageDimensions && (
<div className="flex justify-between text-gray-600">
<span>:</span>
<span className="text-gray-800">{imageDimensions.width} × {imageDimensions.height}</span>
</div>
)}
<div className="flex justify-between text-gray-600 mt-1">
<span>:</span>
<span className="text-gray-800 capitalize">{selectedTool}</span>
</div>
</div>
</div>
)}
</div>
);
};