阶段性提交

This commit is contained in:
2025-09-21 14:43:59 +08:00
parent af2058f752
commit 690a530031
20 changed files with 1577 additions and 781 deletions

View File

@@ -4,7 +4,6 @@ import { Button } from './ui/Button';
import { History, Download, Image as ImageIcon, Trash2 } from 'lucide-react';
import { cn } from '../utils/cn';
import { ImagePreviewModal } from './ImagePreviewModal';
import * as indexedDBService from '../services/indexedDBService';
import { useIndexedDBListener } from '../hooks/useIndexedDBListener';
import { DayPicker } from 'react-day-picker';
import zhCN from 'react-day-picker/dist/locale/zh-CN';
@@ -13,9 +12,8 @@ export const HistoryPanel: React.FC<{
setHoveredImage: (image: {url: string, title: string, width?: number, height?: number} | null) => void,
setPreviewPosition?: (position: {x: number, y: number} | null) => void
}> = ({ setHoveredImage, setPreviewPosition }) => {
const {
const {
currentProject,
canvasImage,
selectedGenerationId,
selectedEditId,
selectGeneration,
@@ -23,7 +21,6 @@ export const HistoryPanel: React.FC<{
showHistory,
setShowHistory,
setCanvasImage,
selectedTool,
removeGeneration,
removeEdit
} = useAppStore();
@@ -68,18 +65,28 @@ export const HistoryPanel: React.FC<{
const [startDate, setStartDate] = useState<string>(() => {
// 初始化时默认显示今天的记录
const today = new Date();
today.setHours(0, 0, 0, 0);
return today.toISOString().split('T')[0];
});
const [endDate, setEndDate] = useState<string>(() => {
// 初始化时默认显示今天的记录
const today = new Date();
today.setHours(0, 0, 0, 0);
return today.toISOString().split('T')[0];
});
const [searchTerm, setSearchTerm] = useState<string>('');
const [showDatePicker, setShowDatePicker] = useState(false); // 控制日期选择器的显示
const [dateRange, setDateRange] = useState<{ from: Date | undefined; to: Date | undefined }>({
from: new Date(new Date().setHours(0, 0, 0, 0)),
to: new Date(new Date().setHours(0, 0, 0, 0))
from: (() => {
const today = new Date();
today.setHours(0, 0, 0, 0);
return today;
})(),
to: (() => {
const today = new Date();
today.setHours(0, 0, 0, 0);
return today;
})()
});
// 分页状态
@@ -87,17 +94,18 @@ export const HistoryPanel: React.FC<{
const itemsPerPage = 20; // 减少每页显示的项目数
// 获取当前图像尺寸
const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null);
// const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null);
// 当currentProject为空时使用dbGenerations和dbEdits作为备选
const displayGenerations = currentProject ? currentProject.generations : dbGenerations;
const displayEdits = currentProject ? currentProject.edits : dbEdits;
// 筛选记录的函数
const filterRecords = useCallback((records: any[], isGeneration: boolean) => {
const filterRecords = useCallback((records: Array<{timestamp: number, prompt?: string, instruction?: string}>, isGeneration: boolean) => {
return records.filter(record => {
// 日期筛选 - 检查记录日期是否在筛选范围内
const recordDate = new Date(record.timestamp);
recordDate.setHours(0, 0, 0, 0); // 设置为当天的开始
const recordDateStr = recordDate.toISOString().split('T')[0]; // 获取日期部分 YYYY-MM-DD
// 检查是否在日期范围内
@@ -108,10 +116,10 @@ export const HistoryPanel: React.FC<{
if (searchTerm) {
if (isGeneration) {
// 生成记录按提示词搜索
return record.prompt.toLowerCase().includes(searchTerm.toLowerCase());
return record.prompt?.toLowerCase().includes(searchTerm.toLowerCase()) || false;
} else {
// 编辑记录按指令搜索
return record.instruction.toLowerCase().includes(searchTerm.toLowerCase());
return record.instruction?.toLowerCase().includes(searchTerm.toLowerCase()) || false;
}
}
@@ -123,17 +131,17 @@ export const HistoryPanel: React.FC<{
const filteredGenerations = useMemo(() => filterRecords(displayGenerations, true), [displayGenerations, filterRecords]);
const filteredEdits = useMemo(() => filterRecords(displayEdits, false), [displayEdits, filterRecords]);
React.useEffect(() => {
if (canvasImage) {
const img = new Image();
img.onload = () => {
setImageDimensions({ width: img.width, height: img.height });
};
img.src = canvasImage;
} else {
setImageDimensions(null);
}
}, [canvasImage]);
// React.useEffect(() => {
// if (canvasImage) {
// const img = new Image();
// img.onload = () => {
// setImageDimensions({ width: img.width, height: img.height });
// };
// img.src = canvasImage;
// } else {
// setImageDimensions(null);
// }
// }, [canvasImage]);
// 当项目变化时解码Blob图像
useEffect(() => {
@@ -196,8 +204,8 @@ export const HistoryPanel: React.FC<{
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_30`; // 降低质量到30%
// 返回原始链接,不添加任何参数
return uploadResult.url;
}
}
return null;
@@ -469,6 +477,13 @@ export const HistoryPanel: React.FC<{
selected={dateRange}
onSelect={(range) => {
if (range) {
// 确保日期时间设置为当天的开始
if (range.from) {
range.from.setHours(0, 0, 0, 0);
}
if (range.to) {
range.to.setHours(0, 0, 0, 0);
}
setDateRange(range);
// 更新字符串格式的日期用于筛选
if (range.from) {
@@ -511,7 +526,8 @@ export const HistoryPanel: React.FC<{
className="text-xs p-1.5 rounded-l-none h-7 border-gray-200 text-gray-600 hover:bg-gray-100 card"
onClick={() => {
// 重置为显示今天的记录
const today = new Date(new Date().setHours(0, 0, 0, 0));
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayStr = today.toISOString().split('T')[0];
setStartDate(todayStr);
setEndDate(todayStr);
@@ -581,7 +597,7 @@ export const HistoryPanel: React.FC<{
sortedGenerations.some(gen => gen.id === record.id)
);
return paginatedGenerations.map((generation, index) => {
return paginatedGenerations.map((generation: {id: string, sourceAssets?: Array<{url: string}>, outputAssets?: Array<{url: string}>}) => {
// 计算全局索引用于显示编号
const globalIndex = allRecords.findIndex(record => record.id === generation.id);
@@ -596,23 +612,57 @@ export const HistoryPanel: React.FC<{
)}
onClick={() => {
selectGeneration(generation.id);
// 设置画布图像为第一个输出资产
if (generation.outputAssets && generation.outputAssets.length > 0) {
// 设置画布图像为参考图像,如果没有参考图像则使用第一个输出资产
let imageUrl = null;
// 首先尝试获取参考图像
if (generation.sourceAssets && generation.sourceAssets.length > 0) {
// 参考图像在uploadResults中从索引outputAssets.length开始
const outputAssetsCount = generation.outputAssets?.length || 0;
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
const uploadedUrl = getUploadedImageUrl(generation, uploadResultIndex);
if (uploadedUrl) {
imageUrl = uploadedUrl;
} else if (generation.sourceAssets[0].url) {
imageUrl = generation.sourceAssets[0].url;
}
}
// 如果没有参考图像,则使用生成结果图像
if (!imageUrl && generation.outputAssets && generation.outputAssets.length > 0) {
const asset = generation.outputAssets[0];
if (asset.url) {
setCanvasImage(asset.url);
const uploadedUrl = getUploadedImageUrl(generation, 0);
imageUrl = uploadedUrl || asset.url;
}
}
if (imageUrl) {
setCanvasImage(imageUrl);
}
}}
onMouseEnter={(e) => {
// 设置当前悬停的记录
setHoveredRecord({type: 'generation', id: generation.id});
// 优先使用上传后的远程链接,如果没有则使用原始链接
let imageUrl = getUploadedImageUrl(generation, 0);
if (!imageUrl && generation.outputAssets && generation.outputAssets.length > 0) {
imageUrl = generation.outputAssets[0].url;
}
// 优先显示参考图像,如果没有参考图像则显示生成结果图像
let imageUrl = null;
// 首先尝试获取参考图像
if (generation.sourceAssets && generation.sourceAssets.length > 0) {
// 参考图像在uploadResults中从索引outputAssets.length开始
const outputAssetsCount = generation.outputAssets?.length || 0;
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
imageUrl = getUploadedImageUrl(generation, uploadResultIndex) ||
(generation.sourceAssets[0].url ? generation.sourceAssets[0].url : null);
}
// 如果没有参考图像,则使用生成结果图像
if (!imageUrl) {
imageUrl = getUploadedImageUrl(generation, 0) ||
(generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null);
}
if (imageUrl) {
// 创建图像对象以获取尺寸
const img = new Image();
@@ -719,9 +769,23 @@ export const HistoryPanel: React.FC<{
}}
>
{(() => {
// 优先使用上传后的远程链接
const imageUrl = getUploadedImageUrl(generation, 0) ||
(generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null);
// 优先显示参考图像,如果没有参考图像则显示生成结果图像
let imageUrl = null;
// 首先尝试获取参考图像
if (generation.sourceAssets && generation.sourceAssets.length > 0) {
// 参考图像在uploadResults中从索引outputAssets.length开始
const outputAssetsCount = generation.outputAssets?.length || 0;
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
imageUrl = getUploadedImageUrl(generation, uploadResultIndex) ||
(generation.sourceAssets[0].url ? generation.sourceAssets[0].url : null);
}
// 如果没有参考图像,则使用生成结果图像
if (!imageUrl) {
imageUrl = getUploadedImageUrl(generation, 0) ||
(generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null);
}
if (imageUrl) {
return <img src={imageUrl} alt="生成的变体" className="w-full h-full object-cover" />;
@@ -748,12 +812,34 @@ export const HistoryPanel: React.FC<{
className="h-8 w-8 bg-white/90 hover:bg-white text-gray-700 rounded-full shadow-md"
onClick={(e) => {
e.stopPropagation();
// 下载图像
const imageUrl = getUploadedImageUrl(generation, 0) ||
(generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null);
// 下载图像 - 优先下载参考图像,如果没有则下载生成结果图像
let imageUrl = null;
// 首先尝试获取参考图像
if (generation.sourceAssets && generation.sourceAssets.length > 0) {
// 参考图像在uploadResults中从索引outputAssets.length开始
const outputAssetsCount = generation.outputAssets?.length || 0;
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
imageUrl = getUploadedImageUrl(generation, uploadResultIndex) ||
(generation.sourceAssets[0].url ? generation.sourceAssets[0].url : null);
}
// 如果没有参考图像,则使用生成结果图像
if (!imageUrl) {
imageUrl = getUploadedImageUrl(generation, 0) ||
(generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null);
}
if (imageUrl) {
// 使用Promise来处理异步操作
fetch(imageUrl)
// 使用fetch获取图像数据并创建Blob URL以确保正确下载
// 添加更多缓存控制头以绕过CDN缓存
fetch(imageUrl, {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
})
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
@@ -767,6 +853,14 @@ export const HistoryPanel: React.FC<{
})
.catch(error => {
console.error('下载图像失败:', error);
// 如果fetch失败回退到直接使用a标签
const link = document.createElement('a');
link.href = imageUrl;
link.download = `nano-banana-${Date.now()}.png`;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
}}
@@ -809,7 +903,7 @@ export const HistoryPanel: React.FC<{
sortedEdits.some(edit => edit.id === record.id)
);
return paginatedEdits.map((edit, index) => {
return paginatedEdits.map((edit: {id: string, sourceAssets?: Array<{url: string}>, outputAssets?: Array<{url: string}>}) => {
// 计算全局索引用于显示编号
const globalIndex = allRecords.findIndex(record => record.id === edit.id);
@@ -825,22 +919,55 @@ export const HistoryPanel: React.FC<{
onClick={() => {
selectEdit(edit.id);
selectGeneration(null);
// 设置画布图像为第一个输出资产
if (edit.outputAssets && edit.outputAssets.length > 0) {
// 设置画布图像为参考图像,如果没有参考图像则使用第一个输出资产
let imageUrl = null;
// 首先尝试获取参考图像
if (edit.sourceAssets && edit.sourceAssets.length > 0) {
// 参考图像在uploadResults中从索引outputAssets.length开始
const outputAssetsCount = edit.outputAssets?.length || 0;
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
const uploadedUrl = getUploadedImageUrl(edit, uploadResultIndex);
if (uploadedUrl) {
imageUrl = uploadedUrl;
} else if (edit.sourceAssets[0].url) {
imageUrl = edit.sourceAssets[0].url;
}
}
// 如果没有参考图像,则使用编辑结果图像
if (!imageUrl && edit.outputAssets && edit.outputAssets.length > 0) {
const asset = edit.outputAssets[0];
if (asset.url) {
setCanvasImage(asset.url);
const uploadedUrl = getUploadedImageUrl(edit, 0);
imageUrl = uploadedUrl || asset.url;
}
}
if (imageUrl) {
setCanvasImage(imageUrl);
}
}}
onMouseEnter={(e) => {
// 设置当前悬停的记录
setHoveredRecord({type: 'edit', id: edit.id});
// 优先使用上传后的远程链接,如果没有则使用原始链接
let imageUrl = getUploadedImageUrl(edit, 0);
if (!imageUrl && edit.outputAssets && edit.outputAssets.length > 0) {
imageUrl = edit.outputAssets[0].url;
// 优先显示参考图像,如果没有参考图像则显示编辑结果图像
let imageUrl = null;
// 首先尝试获取参考图像
if (edit.sourceAssets && edit.sourceAssets.length > 0) {
// 参考图像在uploadResults中从索引outputAssets.length开始
const outputAssetsCount = edit.outputAssets?.length || 0;
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
imageUrl = getUploadedImageUrl(edit, uploadResultIndex) ||
(edit.sourceAssets[0].url ? edit.sourceAssets[0].url : null);
}
// 如果没有参考图像,则使用编辑结果图像
if (!imageUrl) {
imageUrl = getUploadedImageUrl(edit, 0) ||
(edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null);
}
if (imageUrl) {
@@ -889,9 +1016,23 @@ export const HistoryPanel: React.FC<{
}}
>
{(() => {
// 优先使用上传后的远程链接
const imageUrl = getUploadedImageUrl(edit, 0) ||
(edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null);
// 优先显示参考图像,如果没有参考图像则显示编辑结果图像
let imageUrl = null;
// 首先尝试获取参考图像
if (edit.sourceAssets && edit.sourceAssets.length > 0) {
// 参考图像在uploadResults中从索引outputAssets.length开始
const outputAssetsCount = edit.outputAssets?.length || 0;
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
imageUrl = getUploadedImageUrl(edit, uploadResultIndex) ||
(edit.sourceAssets[0].url ? edit.sourceAssets[0].url : null);
}
// 如果没有参考图像,则使用编辑结果图像
if (!imageUrl) {
imageUrl = getUploadedImageUrl(edit, 0) ||
(edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null);
}
if (imageUrl) {
return <img src={imageUrl} alt="编辑的变体" className="w-full h-full object-cover" />;
@@ -918,12 +1059,34 @@ export const HistoryPanel: React.FC<{
className="h-8 w-8 bg-white/90 hover:bg-white text-gray-700 rounded-full shadow-md"
onClick={(e) => {
e.stopPropagation();
// 下载图像
const imageUrl = getUploadedImageUrl(edit, 0) ||
(edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null);
// 下载图像 - 优先下载参考图像,如果没有则下载编辑结果图像
let imageUrl = null;
// 首先尝试获取参考图像
if (edit.sourceAssets && edit.sourceAssets.length > 0) {
// 参考图像在uploadResults中从索引outputAssets.length开始
const outputAssetsCount = edit.outputAssets?.length || 0;
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
imageUrl = getUploadedImageUrl(edit, uploadResultIndex) ||
(edit.sourceAssets[0].url ? edit.sourceAssets[0].url : null);
}
// 如果没有参考图像,则使用编辑结果图像
if (!imageUrl) {
imageUrl = getUploadedImageUrl(edit, 0) ||
(edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null);
}
if (imageUrl) {
// 使用Promise来处理异步操作
fetch(imageUrl)
// 使用fetch获取图像数据并创建Blob URL以确保正确下载
// 添加更多缓存控制头以绕过CDN缓存
fetch(imageUrl, {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
})
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
@@ -937,6 +1100,14 @@ export const HistoryPanel: React.FC<{
})
.catch(error => {
console.error('下载图像失败:', error);
// 如果fetch失败回退到直接使用a标签
const link = document.createElement('a');
link.href = imageUrl;
link.download = `nano-banana-${Date.now()}.png`;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
}}
@@ -1080,7 +1251,7 @@ export const HistoryPanel: React.FC<{
{gen.outputAssets.length}
</div>
<div className="flex flex-wrap gap-2">
{gen.outputAssets.slice(0, 4).map((asset: any, index: number) => {
{gen.outputAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => {
// 获取上传后的远程链接(如果存在)
const uploadedUrl = gen.uploadResults && gen.uploadResults[index] && gen.uploadResults[index].success
? `${gen.uploadResults[index].url}?x-oss-process=image/quality,q_30`
@@ -1169,20 +1340,14 @@ export const HistoryPanel: React.FC<{
{gen.sourceAssets.length}
</div>
<div className="flex flex-wrap gap-2">
{gen.sourceAssets.slice(0, 4).map((asset: any, index: number) => {
// 获取上传后的远程链接如果存在)
// 参考图像在uploadResults中从索引outputAssets.length开始
// 但由于gen可能是轻量级记录我们需要从dbGenerations中获取完整的记录
const fullGen = dbGenerations.find(item => item.id === gen.id) || gen;
const outputAssetsCount = fullGen.outputAssets?.length || 0;
// 确保索引在有效范围内
const uploadResultIndex = outputAssetsCount + index;
const uploadedUrl = fullGen.uploadResults && fullGen.uploadResults[uploadResultIndex] && fullGen.uploadResults[uploadResultIndex].success
? `${fullGen.uploadResults[uploadResultIndex].url}?x-oss-process=image/quality,q_30`
{gen.sourceAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => {
// 优先使用上传后的远程链接如果没有则使用asset中的URL
// 参考图像在uploadResults中从索引1开始图像2字段
const uploadResultIndex = 1 + index;
const uploadedUrl = gen.uploadResults && gen.uploadResults[uploadResultIndex] && gen.uploadResults[uploadResultIndex].success
? gen.uploadResults[uploadResultIndex].url
: null;
// 如果没有上传的URL则使用asset中的URL
const displayUrl = uploadedUrl || asset.url || asset.blobUrl;
// 如果URL是blob:开头但已失效,尝试重新创建
@@ -1320,10 +1485,10 @@ export const HistoryPanel: React.FC<{
{selectedEdit.outputAssets.length}
</div>
<div className="flex flex-wrap gap-2">
{selectedEdit.outputAssets.slice(0, 4).map((asset: any, index: number) => {
{selectedEdit.outputAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => {
// 获取上传后的远程链接(如果存在)
const uploadedUrl = selectedEdit.uploadResults && selectedEdit.uploadResults[index] && selectedEdit.uploadResults[index].success
? `${selectedEdit.uploadResults[index].url}?x-oss-process=image/quality,q_30`
? selectedEdit.uploadResults[index].url
: null;
// 如果没有上传的URL则使用asset中的URL
@@ -1401,6 +1566,96 @@ export const HistoryPanel: React.FC<{
</div>
)}
{/* 编辑参考图像 */}
{selectedEdit.sourceAssets && selectedEdit.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">
{selectedEdit.sourceAssets.length}
</div>
<div className="flex flex-wrap gap-2">
{selectedEdit.sourceAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => {
// 优先使用上传后的远程链接如果没有则使用asset中的URL
// 参考图像在uploadResults中从索引1开始图像2字段
const uploadResultIndex = 1 + index;
const uploadedUrl = selectedEdit.uploadResults && selectedEdit.uploadResults[uploadResultIndex] && selectedEdit.uploadResults[uploadResultIndex].success
? selectedEdit.uploadResults[uploadResultIndex].url
: null;
const displayUrl = uploadedUrl || asset.url || asset.blobUrl;
// 如果URL是blob:开头但已失效,尝试重新创建
if (displayUrl && displayUrl.startsWith('blob:')) {
// 检查blob是否仍然有效
const img = new Image();
img.onerror = () => {
// Blob URL可能已失效尝试重新创建
import('../store/useAppStore').then((module) => {
const useAppStore = module.useAppStore;
const blob = useAppStore.getState().getBlob(displayUrl);
if (blob) {
const newUrl = URL.createObjectURL(blob);
// 更新显示
const imgElement = document.querySelector(`img[src="${displayUrl}"]`);
if (imgElement) {
imgElement.src = newUrl;
}
}
});
};
img.src = displayUrl;
}
return (
<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: displayUrl,
title: `编辑参考图像 ${index + 1}`,
description: `${asset.width} × ${asset.height}`
});
}}
>
{displayUrl ? (
<img
src={displayUrl}
alt={`编辑参考图像 ${index + 1}`}
className="w-full h-full object-cover"
onError={(e) => {
// 如果图像加载失败尝试重新创建Blob URL
if (displayUrl.startsWith('blob:')) {
import('../store/useAppStore').then((module) => {
const useAppStore = module.useAppStore;
const blob = useAppStore.getState().getBlob(displayUrl);
if (blob) {
const newUrl = URL.createObjectURL(blob);
(e.target as HTMLImageElement).src = newUrl;
}
});
}
}}
/>
) : (
<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>
);
})}
{selectedEdit.sourceAssets.length > 4 && (
<div className="w-16 h-16 rounded border flex items-center justify-center bg-gray-100 text-xs text-gray-500">
+{selectedEdit.sourceAssets.length - 4}
</div>
)}
</div>
</div>
)}
{/* 原始生成参考 */}
{parentGen && (
<div className="pt-2 border-t border-gray-200">
@@ -1415,18 +1670,14 @@ export const HistoryPanel: React.FC<{
:
</div>
<div className="flex flex-wrap gap-2">
{parentGen.sourceAssets.slice(0, 4).map((asset: any, index: number) => {
// 获取上传后的远程链接如果存在)
// 参考图像在uploadResults中从索引outputAssets.length开始
const outputAssetsCount = parentGen.outputAssets?.length || 0;
// 确保索引在有效范围内
const uploadResultIndex = outputAssetsCount + index;
{parentGen.sourceAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => {
// 优先使用上传后的远程链接如果没有则使用asset中的URL
// 参考图像在uploadResults中从索引1开始图像2字段
const uploadResultIndex = 1 + index;
const uploadedUrl = parentGen.uploadResults && parentGen.uploadResults[uploadResultIndex] && parentGen.uploadResults[uploadResultIndex].success
? `${parentGen.uploadResults[uploadResultIndex].url}?x-oss-process=image/quality,q_30`
? parentGen.uploadResults[uploadResultIndex].url
: null;
// 如果没有上传的URL则使用asset中的URL
const displayUrl = uploadedUrl || asset.url || asset.blobUrl;
// 如果URL是blob:开头但已失效,尝试重新创建

View File

@@ -2,25 +2,19 @@ import React, { useRef, useEffect, useState, useCallback } from 'react';
import { Stage, Layer, Image as KonvaImage, Line } from 'react-konva';
import { useAppStore } from '../store/useAppStore';
import { Button } from './ui/Button';
import { ZoomIn, ZoomOut, RotateCcw, Download, Eye, EyeOff, Eraser } from 'lucide-react';
import { cn } from '../utils/cn';
import { ZoomIn, ZoomOut, RotateCcw, Download } from 'lucide-react';
export const ImageCanvas: React.FC = () => {
const {
canvasImage,
canvasZoom,
setCanvasZoom,
canvasPan,
setCanvasPan,
brushStrokes,
addBrushStroke,
clearBrushStrokes,
showMasks,
setShowMasks,
selectedTool,
isGenerating,
brushSize,
setBrushSize,
showHistory,
showPromptPanel
} = useAppStore();
@@ -50,7 +44,7 @@ export const ImageCanvas: React.FC = () => {
}
}, 0);
}
}, []);
}, [setCanvasZoom]);
// 加载图像
useEffect(() => {
@@ -60,7 +54,7 @@ export const ImageCanvas: React.FC = () => {
console.log('开始加载图像URL:', canvasImage);
img = new window.Image();
let isCancelled = false;
const isCancelled = false;
img.onload = () => {
// 检查是否已取消
@@ -195,7 +189,7 @@ export const ImageCanvas: React.FC = () => {
image.src = '';
}
};
}, [canvasImage]); // 只依赖canvasImage避免其他依赖引起循环
}, [canvasImage, image, setCanvasZoom, stageSize.height, stageSize.width]); // 添加所有依赖项
// 处理舞台大小调整
useEffect(() => {
@@ -212,7 +206,7 @@ export const ImageCanvas: React.FC = () => {
updateSize();
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
}, []);
}, [showPromptPanel, showHistory]);
// 监听面板状态变化以调整画布大小
useEffect(() => {
@@ -243,14 +237,13 @@ export const ImageCanvas: React.FC = () => {
container.addEventListener('wheel', handleWheel, { passive: false });
return () => container.removeEventListener('wheel', handleWheel);
}, [canvasZoom]);
}, [canvasZoom, handleZoom]);
const handleMouseDown = (e: any) => {
const handleMouseDown = (e: Konva.KonvaEventObject<MouseEvent>) => {
if (selectedTool !== 'mask' || !image) return;
setIsDrawing(true);
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
// 使用 Konva 的 getRelativePointerPosition 获取准确坐标
const relativePos = stage.getRelativePointerPosition();
@@ -269,11 +262,10 @@ export const ImageCanvas: React.FC = () => {
}
};
const handleMouseMove = (e: any) => {
const handleMouseMove = (e: Konva.KonvaEventObject<MouseEvent>) => {
if (!isDrawing || selectedTool !== 'mask' || !image) return;
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
// 使用 Konva 的 getRelativePointerPosition 获取准确坐标
const relativePos = stage.getRelativePointerPosition();
@@ -351,17 +343,17 @@ export const ImageCanvas: React.FC = () => {
// 下载第一个上传结果(通常是生成的图像)
const uploadResult = selectedRecord.uploadResults[0];
if (uploadResult.success && uploadResult.url) {
// 使用async IIFE处理异步操作
(async () => {
try {
// 首先尝试使用fetch获取图像数据
const response = await fetch(uploadResult.url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
// 创建下载链接
// 使用fetch获取图像数据并创建Blob URL以确保正确下载
// 添加更多缓存控制头以绕过CDN缓存
fetch(uploadResult.url, {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
})
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
@@ -369,64 +361,21 @@ export const ImageCanvas: React.FC = () => {
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 清理创建的URL
setTimeout(() => URL.revokeObjectURL(url), 100);
console.log('上传后的图像下载成功:', uploadResult.url);
} catch (error) {
console.error('使用fetch下载上传后的图像时出错:', error);
// 如果fetch失败可能是跨域问题使用Canvas方案
try {
const img = new Image();
img.crossOrigin = 'Anonymous'; // 设置跨域属性
img.onload = () => {
try {
// 创建canvas并绘制图像
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
// 将canvas转换为blob并下载
canvas.toBlob((blob) => {
if (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
setTimeout(() => URL.revokeObjectURL(url), 100);
console.log('使用Canvas方案下载成功');
} else {
console.error('Canvas转换为blob失败');
}
}, 'image/png');
} catch (canvasError) {
console.error('Canvas处理失败:', canvasError);
}
};
img.onerror = (imgError) => {
console.error('图像加载失败:', imgError);
console.log('下载失败,未执行回退方案');
};
img.src = uploadResult.url;
} catch (canvasError) {
console.error('Canvas方案也失败了:', canvasError);
console.log('下载失败,未执行回退方案');
}
}
})();
URL.revokeObjectURL(url);
})
.catch(error => {
console.error('下载图像失败:', error);
// 如果fetch失败回退到直接使用a标签
const link = document.createElement('a');
link.href = uploadResult.url;
link.download = `nano-banana-${Date.now()}.png`;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
// 立即返回,让异步操作在后台进行
// 立即返回
return;
}
}
@@ -463,14 +412,17 @@ export const ImageCanvas: React.FC = () => {
document.body.removeChild(link);
} else if (canvasImage.startsWith('blob:')) {
// Blob URL格式
// 使用async IIFE处理异步操作
(async () => {
try {
const response = await fetch(canvasImage);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
// 使用fetch获取图像数据并创建Blob URL以确保正确下载
// 添加更多缓存控制头以绕过CDN缓存
fetch(canvasImage, {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
})
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
@@ -478,68 +430,32 @@ export const ImageCanvas: React.FC = () => {
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 清理创建的URL
setTimeout(() => URL.revokeObjectURL(url), 100);
} catch (error) {
console.error('下载Blob图像时出错:', error);
// 如果fetch失败可能是跨域问题使用Canvas方案
try {
const img = new Image();
img.onload = () => {
try {
// 创建canvas并绘制图像
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
// 将canvas转换为blob并下载
canvas.toBlob((blob) => {
if (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
setTimeout(() => URL.revokeObjectURL(url), 100);
console.log('使用Canvas方案下载成功');
} else {
console.error('Canvas转换为blob失败');
}
}, 'image/png');
} catch (canvasError) {
console.error('Canvas处理失败:', canvasError);
}
};
img.onerror = (imgError) => {
console.error('图像加载失败:', imgError);
console.log('下载失败,未执行回退方案');
};
img.src = canvasImage;
} catch (canvasError) {
console.error('Canvas方案也失败了:', canvasError);
console.log('下载失败,未执行回退方案');
}
}
})();
URL.revokeObjectURL(url);
})
.catch(error => {
console.error('下载图像失败:', error);
// 如果fetch失败回退到直接使用a标签
const link = document.createElement('a');
link.href = canvasImage;
link.download = `nano-banana-${Date.now()}.png`;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
} else {
// 普通URL格式
// 使用async IIFE处理异步操作
(async () => {
try {
const response = await fetch(canvasImage);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
// 使用fetch获取图像数据并创建Blob URL以确保正确下载
// 添加更多缓存控制头以绕过CDN缓存
fetch(canvasImage, {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
})
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
@@ -547,59 +463,19 @@ export const ImageCanvas: React.FC = () => {
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 清理创建的URL
setTimeout(() => URL.revokeObjectURL(url), 100);
} catch (error) {
console.error('下载图像时出错:', error);
// 如果fetch失败可能是跨域问题使用Canvas方案
try {
const img = new Image();
img.crossOrigin = 'Anonymous'; // 设置跨域属性
img.onload = () => {
try {
// 创建canvas并绘制图像
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
// 将canvas转换为blob并下载
canvas.toBlob((blob) => {
if (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
setTimeout(() => URL.revokeObjectURL(url), 100);
console.log('使用Canvas方案下载成功');
} else {
console.error('Canvas转换为blob失败');
}
}, 'image/png');
} catch (canvasError) {
console.error('Canvas处理失败:', canvasError);
}
};
img.onerror = (imgError) => {
console.error('图像加载失败:', imgError);
console.log('下载失败,未执行回退方案');
};
img.src = canvasImage;
} catch (canvasError) {
console.error('Canvas方案也失败了:', canvasError);
console.log('下载失败,未执行回退方案');
}
}
})();
URL.revokeObjectURL(url);
})
.catch(error => {
console.error('下载图像失败:', error);
// 如果fetch失败回退到直接使用a标签
const link = document.createElement('a');
link.href = canvasImage;
link.download = `nano-banana-${Date.now()}.png`;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
}
}
@@ -647,7 +523,7 @@ export const ImageCanvas: React.FC = () => {
width={stageSize.width}
height={stageSize.height}
draggable={selectedTool !== 'mask'}
onDragEnd={(e) => {
onDragEnd={() => {
// 通过stageRef直接获取和设置位置
const stage = stageRef.current;
if (stage) {

View File

@@ -1,13 +1,101 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { Textarea } from './ui/Textarea';
import { Button } from './ui/Button';
import { useAppStore } from '../store/useAppStore';
import { useImageGeneration, useImageEditing } from '../hooks/useImageGeneration';
import { Upload, Wand2, Edit3, MousePointer, HelpCircle, ChevronDown, ChevronRight, RotateCcw } from 'lucide-react';
import { blobToBase64, urlToBlob } from '../utils/imageUtils';
import { urlToBlob } from '../utils/imageUtils';
import { PromptHints } from './PromptHints';
import { PromptSuggestions } from './PromptSuggestions';
import { cn } from '../utils/cn';
import * as referenceImageService from '../services/referenceImageService';
// 图像预览组件
const ImagePreview: React.FC<{
image: string;
index: number;
selectedTool: 'generate' | 'edit' | 'mask';
onRemove: () => void;
}> = ({ image, index, onRemove }) => {
const [imageSrc, setImageSrc] = useState<string>(image);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<boolean>(false);
useEffect(() => {
// 如果是IndexedDB图像需要获取实际的Blob数据
if (image.startsWith('indexeddb://')) {
const imageId = image.replace('indexeddb://', '');
referenceImageService.getReferenceImage(imageId)
.then(blob => {
if (blob) {
const url = URL.createObjectURL(blob);
setImageSrc(url);
setLoading(false);
} else {
setError(true);
setLoading(false);
}
})
.catch(err => {
console.error('获取参考图像失败:', err);
setError(true);
setLoading(false);
});
} else {
// 对于其他类型的URL直接使用
setImageSrc(image);
setLoading(false);
}
}, [image]);
// 清理创建的Blob URL
useEffect(() => {
return () => {
if (imageSrc.startsWith('blob:') && imageSrc !== image) {
URL.revokeObjectURL(imageSrc);
}
};
}, [imageSrc, image]);
if (loading) {
return (
<div className="relative w-full h-20 rounded-lg border-2 border-gray-200 bg-gray-100 flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-500"></div>
</div>
);
}
if (error) {
return (
<div className="relative w-full h-20 rounded-lg border-2 border-gray-200 bg-red-50 flex items-center justify-center">
<span className="text-red-500 text-sm"></span>
</div>
);
}
return (
<div className="relative">
<img
src={imageSrc}
alt={`参考图像 ${index + 1}`}
className="w-full h-20 object-cover rounded-lg border-2 border-gray-200"
onError={() => setError(true)}
/>
<button
onClick={onRemove}
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1.5 transition-colors shadow-md hover:bg-red-600"
>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<div className="absolute bottom-2 left-2 bg-black/60 text-white text-xs px-2 py-1 rounded-lg">
{index + 1}
</div>
</div>
);
};
export const PromptComposer: React.FC = () => {
const {
@@ -32,8 +120,7 @@ export const PromptComposer: React.FC = () => {
setCanvasImage,
showPromptPanel,
setShowPromptPanel,
clearBrushStrokes,
addBlob
clearBrushStrokes
} = useAppStore();
const { generate, cancelGeneration } = useImageGeneration();
@@ -45,12 +132,27 @@ export const PromptComposer: React.FC = () => {
const [isDragOver, setIsDragOver] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// 初始化参考图像数据库
useEffect(() => {
const initDB = async () => {
try {
await referenceImageService.initReferenceImageDB();
console.log('参考图像数据库初始化成功');
} catch (error) {
console.error('参考图像数据库初始化失败:', error);
}
};
initDB();
}, []);
const handleGenerate = async () => {
if (!currentPrompt.trim()) return;
if (selectedTool === 'generate') {
// 将上传的图像转换为Blob对象
const referenceImageBlobs: Blob[] = [];
for (const img of uploadedImages) {
if (img.startsWith('data:')) {
// 从base64数据创建Blob
@@ -63,6 +165,23 @@ export const PromptComposer: React.FC = () => {
ia[i] = byteString.charCodeAt(i);
}
referenceImageBlobs.push(new Blob([ab], { type: mimeString }));
} else if (img.startsWith('indexeddb://')) {
// 从IndexedDB获取参考图像
const imageId = img.replace('indexeddb://', '');
try {
const blob = await referenceImageService.getReferenceImage(imageId);
if (blob) {
referenceImageBlobs.push(blob);
} else {
console.warn('无法从IndexedDB获取参考图像:', imageId);
// 如果无法获取图像,尝试重新上传
console.log('尝试重新处理参考图像...');
}
} catch (error) {
console.warn('无法从IndexedDB获取参考图像:', imageId, error);
// 如果无法获取图像,尝试重新上传
console.log('尝试重新处理参考图像...');
}
} else if (img.startsWith('blob:')) {
// 从Blob URL获取Blob
const { getBlob } = useAppStore.getState();
@@ -75,17 +194,9 @@ export const PromptComposer: React.FC = () => {
const response = await fetch(img);
if (response.ok) {
const blob = await response.blob();
// 重新添加到AppStore
const newUrl = useAppStore.getState().addBlob(blob);
referenceImageBlobs.push(blob);
// 更新uploadedImages中的URL
const index = uploadedImages.indexOf(img);
if (index !== -1) {
const newImages = [...uploadedImages];
newImages[index] = newUrl;
useAppStore.getState().clearUploadedImages();
newImages.forEach(imageUrl => useAppStore.getState().addUploadedImage(imageUrl));
}
} else {
console.warn('无法重新获取参考图像:', img);
}
} catch (error) {
console.warn('无法重新获取参考图像:', img, error);
@@ -102,9 +213,13 @@ export const PromptComposer: React.FC = () => {
}
}
// 过滤掉无效的Blob只保留有效的参考图像
const validBlobs = referenceImageBlobs.filter(blob => blob.size > 0);
// 即使没有参考图像也继续生成,因为提示文本是必需的
generate({
prompt: currentPrompt,
referenceImages: referenceImageBlobs.length > 0 ? referenceImageBlobs : undefined,
referenceImages: validBlobs.length > 0 ? validBlobs : undefined,
temperature,
seed: seed !== null ? seed : undefined
});
@@ -116,28 +231,36 @@ export const PromptComposer: React.FC = () => {
const handleFileUpload = async (file: File) => {
if (file && file.type.startsWith('image/')) {
try {
// 直接使用Blob创建URL
const blobUrl = addBlob(file);
// 保存参考图像到IndexedDB
const imageId = await referenceImageService.saveReferenceImage(file);
// 创建一个特殊的URL来标识这是存储在IndexedDB中的图像
const imageUrl = `indexeddb://${imageId}`;
if (selectedTool === 'generate') {
// 添加到参考图像最多2张
if (uploadedImages.length < 2) {
addUploadedImage(blobUrl);
addUploadedImage(imageUrl);
}
} else if (selectedTool === 'edit') {
// 编辑模式下添加到单独的编辑参考图像最多2张
if (editReferenceImages.length < 2) {
addEditReferenceImage(blobUrl);
addEditReferenceImage(imageUrl);
}
// 如果没有画布图像,则设置为画布图像
if (!canvasImage) {
setCanvasImage(blobUrl);
setCanvasImage(imageUrl);
}
} else if (selectedTool === 'mask') {
// 遮罩模式下,立即设置为画布图像
clearUploadedImages();
addUploadedImage(blobUrl);
setCanvasImage(blobUrl);
// 遮罩模式下,将图像添加为参考图像而不是清除现有图像
// 只有在没有画布图像时才设置为画布图像
if (!canvasImage) {
setCanvasImage(imageUrl);
}
// 不清除现有的上传图像,而是将新图像添加为参考图像(如果还有空间)
if (uploadedImages.length < 2) {
addUploadedImage(imageUrl);
}
}
} catch (error) {
console.error('上传图像失败:', error);
@@ -171,7 +294,7 @@ export const PromptComposer: React.FC = () => {
}
};
const handleClearSession = () => {
const handleClearSession = async () => {
setCurrentPrompt('');
clearUploadedImages();
clearEditReferenceImages();
@@ -180,6 +303,14 @@ export const PromptComposer: React.FC = () => {
setSeed(null);
setTemperature(0.7);
setShowClearConfirm(false);
// 清空IndexedDB中的所有参考图像
try {
await referenceImageService.clearAllReferenceImages();
console.log('已清空IndexedDB中的所有参考图像');
} catch (error) {
console.error('清空IndexedDB中的参考图像失败:', error);
}
};
const tools = [
@@ -324,25 +455,13 @@ export const PromptComposer: React.FC = () => {
(selectedTool === 'edit' && editReferenceImages.length > 0)) && (
<div className="space-y-2.5">
{(selectedTool === 'generate' ? uploadedImages : editReferenceImages).map((image, index) => (
<div key={index} className="relative">
<img
src={image}
alt={`参考图像 ${index + 1}`}
className="w-full h-20 object-cover rounded-lg border-2 border-gray-200"
/>
<button
onClick={() => selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)}
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1.5 transition-colors shadow-md hover:bg-red-600"
>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<div className="absolute bottom-2 left-2 bg-black/60 text-white text-xs px-2 py-1 rounded-lg">
{index + 1}
</div>
</div>
<ImagePreview
key={index}
image={image}
index={index}
selectedTool={selectedTool}
onRemove={() => selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)}
/>
))}
</div>
)}
@@ -423,7 +542,7 @@ export const PromptComposer: React.FC = () => {
<div className="mt-4 animate-in slide-down duration-300">
<PromptSuggestions
onWordSelect={(word) => {
setCurrentPrompt(currentPrompt ? `${currentPrompt}, ${word}` : word);
setCurrentPrompt(currentPrompt ? `${currentPrompt}${word}` : word);
}}
minFrequency={3}
showTitle={false}
@@ -493,7 +612,7 @@ export const PromptComposer: React.FC = () => {
<div className="flex space-x-3">
<Button
variant="destructive"
onClick={handleClearSession}
onClick={async () => await handleClearSession()}
className="flex-1 h-10 text-sm font-semibold card text-gray-700"
>

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { Button } from './ui/Button';
import { useAppStore } from '../store/useAppStore';
import { cn } from '../utils/cn';
@@ -18,17 +18,17 @@ export const PromptSuggestions: React.FC<{
const [showAll, setShowAll] = useState(false);
// 从提示词中提取词语并统计频次
const extractWords = (text: string): string[] => {
const extractWords = useCallback((text: string): string[] => {
// 移除标点符号并分割词语
return text
.toLowerCase()
.replace(/[^\w\s\u4e00-\u9fff]/g, ' ') // 保留中文字符
.split(/\s+/)
.filter(word => word.length > 1); // 过滤掉单字符
};
}, []);
// 统计词语频次
const calculateWordFrequency = (): WordFrequency[] => {
const calculateWordFrequency = useCallback((): WordFrequency[] => {
const wordCount: Record<string, number> = {};
// 收集所有提示词
@@ -52,7 +52,7 @@ export const PromptSuggestions: React.FC<{
});
}
// 提取词语并统计频次
// 统计词语频次
allPrompts.forEach(prompt => {
const words = extractWords(prompt);
words.forEach(word => {
@@ -65,11 +65,11 @@ export const PromptSuggestions: React.FC<{
.map(([word, count]) => ({ word, count }))
.filter(({ count }) => count >= minFrequency)
.sort((a, b) => b.count - a.count);
};
}, [currentProject, minFrequency, extractWords]);
useEffect(() => {
setFrequentWords(calculateWordFrequency());
}, [currentProject, minFrequency]);
}, [currentProject, minFrequency, calculateWordFrequency]);
// 显示的词语数量
const displayWords = showAll ? frequentWords : frequentWords.slice(0, 20);

View File

@@ -1,4 +1,4 @@
import React, { createContext, useContext, useReducer, useState, useEffect, useRef } from 'react';
import React, { createContext, useContext, useEffect, useReducer, useRef } from 'react';
import { Toast } from './Toast';
type ToastType = 'success' | 'error' | 'warning' | 'info';

View File

@@ -1,22 +1,38 @@
import React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../utils/cn';
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement>,
VariantProps<typeof inputVariants> {
// Additional props can be added here if needed
}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
const inputVariants = cva(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
{
variants: {
variant: {
default: 'border-gray-300 focus-visible:ring-yellow-400',
},
},
defaultVariants: {
variant: 'default',
},
}
);
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, variant, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'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={cn(inputVariants({ variant, className }))}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = 'Input';
Input.displayName = 'Input';
export { Input };

View File

@@ -1,7 +1,11 @@
import React from 'react';
import { cn } from '../../utils/cn';
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement>,
VariantProps<typeof textareaVariants> {
// Additional props can be added here if needed
}
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {