优化 调整了整体的UI界面

This commit is contained in:
2025-09-15 22:48:42 +08:00
parent a922a4e507
commit b3cbba41bb
4 changed files with 208 additions and 217 deletions

View File

@@ -8,29 +8,24 @@ export const Header: React.FC = () => {
return ( return (
<> <>
<header className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6"> <header className="h-12 bg-white border-b border-gray-100 flex items-center justify-between px-3">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-2">
<div className="flex items-center space-x-2"> <div className="flex items-center justify-center w-8 h-8 rounded-lg bg-yellow-50">
<div className="text-2xl">🍌</div> <div className="text-lg">🍌</div>
<h1 className="text-xl font-semibold text-gray-900 hidden md:block">
Nano Banana AI
</h1>
<h1 className="text-xl font-semibold text-gray-900 md:hidden">
NB
</h1>
</div>
<div className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
1.0
</div> </div>
<h1 className="text-base font-medium text-gray-800 hidden sm:block">
Nano Banana
</h1>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-1">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => setShowInfoModal(true)} onClick={() => setShowInfoModal(true)}
className="h-7 w-7 text-gray-500 hover:text-gray-700 hover:bg-gray-100"
> >
<HelpCircle className="h-5 w-5" /> <HelpCircle className="h-4 w-4" />
</Button> </Button>
</div> </div>
</header> </header>

View File

@@ -211,19 +211,19 @@ export const HistoryPanel: React.FC = () => {
} }
return ( return (
<div className="w-80 bg-white border-l border-gray-200 p-6 flex flex-col h-full"> <div className="w-72 bg-white border-l border-gray-200 p-4 flex flex-col h-full">
{/* 头部 */} {/* 头部 */}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<History className="h-5 w-5 text-gray-400" /> <History className="h-4 w-4 text-gray-500" />
<h3 className="text-sm font-medium text-gray-300"></h3> <h3 className="text-sm font-medium text-gray-700"></h3>
</div> </div>
<div className="flex space-x-1"> <div className="flex space-x-1">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={refresh} onClick={refresh}
className="h-6 w-6" className="h-6 w-6 text-gray-400 hover:text-gray-600 hover:bg-gray-100"
title="刷新历史记录" title="刷新历史记录"
> >
@@ -232,29 +232,32 @@ export const HistoryPanel: React.FC = () => {
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => setShowHistory(!showHistory)} onClick={() => setShowHistory(!showHistory)}
className="h-6 w-6" className="h-6 w-6 text-gray-400 hover:text-gray-600 hover:bg-gray-100"
title="隐藏历史面板" 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> </Button>
</div> </div>
</div> </div>
{/* 筛选和搜索控件 */} {/* 筛选和搜索控件 */}
<div className="mb-4 space-y-3"> <div className="mb-3 space-y-2">
<div className="flex space-x-2"> <div className="flex space-x-1">
<input <input
type="date" type="date"
value={startDate} value={startDate}
onChange={(e) => setStartDate(e.target.value)} onChange={(e) => setStartDate(e.target.value)}
className="flex-1 text-xs p-1 border rounded" className="flex-1 text-xs p-1.5 border border-gray-200 rounded text-gray-600 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-yellow-400"
placeholder="开始日期" placeholder="开始日期"
/> />
<input <input
type="date" type="date"
value={endDate} value={endDate}
onChange={(e) => setEndDate(e.target.value)} onChange={(e) => setEndDate(e.target.value)}
className="flex-1 text-xs p-1 border rounded" className="flex-1 text-xs p-1.5 border border-gray-200 rounded text-gray-600 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-yellow-400"
placeholder="结束日期" placeholder="结束日期"
/> />
</div> </div>
@@ -263,13 +266,13 @@ export const HistoryPanel: React.FC = () => {
type="text" type="text"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 text-xs p-1 border rounded-l" className="flex-1 text-xs p-1.5 border border-gray-200 rounded-l bg-gray-50 focus:outline-none focus:ring-1 focus:ring-yellow-400"
placeholder="搜索提示词或编辑指令..." placeholder="搜索提示词..."
/> />
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="text-xs p-1 rounded-l-none" className="text-xs p-1.5 rounded-l-none h-7 border-gray-200 text-gray-600 hover:bg-gray-100"
onClick={() => { onClick={() => {
setStartDate(''); setStartDate('');
setEndDate(''); setEndDate('');
@@ -283,28 +286,28 @@ export const HistoryPanel: React.FC = () => {
{/* 变体网格 */} {/* 变体网格 */}
<div className="mb-6 flex-shrink-0"> <div className="mb-6 flex-shrink-0">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-2">
<h4 className="text-xs font-medium text-gray-400"></h4> <h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide"></h4>
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-400">
{filteredGenerations.length + filteredEdits.length}/1000 {filteredGenerations.length + filteredEdits.length}/1000
</span> </span>
</div> </div>
{filteredGenerations.length === 0 && filteredEdits.length === 0 ? ( {filteredGenerations.length === 0 && filteredEdits.length === 0 ? (
<div className="text-center py-8"> <div className="text-center py-8">
<div className="text-4xl mb-2">🖼</div> <div className="text-2xl mb-2 text-gray-300">🖼</div>
<p className="text-sm text-gray-500"></p> <p className="text-xs text-gray-400"></p>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-3 gap-2 max-h-80 overflow-y-auto"> <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) => ( {[...filteredGenerations].sort((a, b) => b.timestamp - a.timestamp).slice(0, 50).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 border cursor-pointer transition-all duration-200 overflow-hidden',
selectedGenerationId === generation.id selectedGenerationId === generation.id
? 'border-yellow-400' ? 'border-yellow-400'
: 'border-gray-300 hover:border-gray-400' : 'border-gray-200 hover:border-gray-300'
)} )}
onClick={() => { onClick={() => {
selectGeneration(generation.id); selectGeneration(generation.id);
@@ -400,10 +403,10 @@ export const HistoryPanel: React.FC = () => {
<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 border cursor-pointer transition-all duration-200 overflow-hidden',
selectedEditId === edit.id selectedEditId === edit.id
? 'border-purple-400' ? 'border-purple-400'
: 'border-gray-300 hover:border-gray-400' : 'border-gray-200 hover:border-gray-300'
)} )}
onClick={() => { onClick={() => {
selectEdit(edit.id); selectEdit(edit.id);
@@ -518,56 +521,56 @@ export const HistoryPanel: React.FC = () => {
)} )}
{/* 生成详情 */} {/* 生成详情 */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200 flex-1 overflow-y-auto min-h-0"> <div className="flex-1 overflow-y-auto min-h-0">
<h4 className="text-xs font-medium text-gray-500 mb-2"></h4> <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 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); const selectedEdit = filteredEdits.find(e => e.id === selectedEditId) || dbEdits.find(e => e.id === selectedEditId);
if (gen) { if (gen) {
return ( return (
<div className="space-y-3"> <div className="space-y-3 p-3 bg-gray-50 rounded-lg">
<div className="space-y-2 text-xs text-gray-600"> <div className="space-y-2.5 text-xs text-gray-700">
<div> <div>
<span className="text-gray-500">:</span> <span className="text-gray-500">:</span>
<p className="text-gray-800 mt-1">{gen.prompt}</p> <p className="text-gray-800 mt-1 leading-relaxed">{gen.prompt}</p>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>:</span> <span className="text-gray-500">:</span>
<span>{gen.modelVersion}</span> <span className="text-gray-700">{gen.modelVersion}</span>
</div> </div>
{gen.parameters.seed && ( {gen.parameters.seed && (
<div className="flex justify-between"> <div className="flex justify-between">
<span>:</span> <span className="text-gray-500">:</span>
<span>{gen.parameters.seed}</span> <span className="text-gray-700 font-mono">{gen.parameters.seed}</span>
</div> </div>
)} )}
<div className="flex justify-between"> <div className="flex justify-between">
<span>:</span> <span className="text-gray-500">:</span>
<span>{new Date(gen.timestamp).toLocaleString()}</span> <span className="text-gray-700">{new Date(gen.timestamp).toLocaleString()}</span>
</div> </div>
</div> </div>
{/* 上传结果 */} {/* 上传结果 */}
{gen.uploadResults && gen.uploadResults.length > 0 && ( {gen.uploadResults && gen.uploadResults.length > 0 && (
<div> <div className="pt-2 border-t border-gray-200">
<h5 className="text-xs font-medium text-gray-500 mb-2"></h5> <h5 className="text-xs font-medium text-gray-500 mb-2"></h5>
<div className="space-y-1"> <div className="space-y-1.5">
{gen.uploadResults.map((result, index) => ( {gen.uploadResults.map((result, index) => (
<div key={index} className="text-xs"> <div key={index} className="text-xs">
<div className="flex justify-between"> <div className="flex justify-between">
<span> {index + 1}:</span> <span className="text-gray-500"> {index + 1}:</span>
<span className={result.success ? 'text-green-600' : 'text-red-600'}> <span className={result.success ? 'text-green-600' : 'text-red-600'}>
{result.success ? '成功' : '失败'} {result.success ? '成功' : '失败'}
</span> </span>
</div> </div>
{result.success && result.url && ( {result.success && result.url && (
<div className="text-blue-600 truncate"> <div className="text-blue-600 truncate text-xs mt-0.5">
{result.url.split('/').pop()} {result.url.split('/').pop()}
</div> </div>
)} )}
{result.error && ( {result.error && (
<div className="text-red-600 truncate"> <div className="text-red-600 truncate text-xs mt-0.5">
{result.error} {result.error}
</div> </div>
)} )}
@@ -579,7 +582,7 @@ export const HistoryPanel: React.FC = () => {
{/* 参考图像信息 */} {/* 参考图像信息 */}
{gen.sourceAssets && gen.sourceAssets.length > 0 && ( {gen.sourceAssets && gen.sourceAssets.length > 0 && (
<div> <div className="pt-2 border-t border-gray-200">
<h5 className="text-xs font-medium text-gray-500 mb-2"></h5> <h5 className="text-xs font-medium text-gray-500 mb-2"></h5>
<div className="text-xs text-gray-600 mb-2"> <div className="text-xs text-gray-600 mb-2">
{gen.sourceAssets.length} {gen.sourceAssets.length}
@@ -588,7 +591,7 @@ export const HistoryPanel: React.FC = () => {
{gen.sourceAssets.slice(0, 4).map((asset: any, index: number) => ( {gen.sourceAssets.slice(0, 4).map((asset: any, index: number) => (
<div <div
key={asset.id} key={asset.id}
className="relative w-16 h-16 rounded border overflow-hidden cursor-pointer" className="relative w-16 h-16 rounded border overflow-hidden cursor-pointer hover:ring-2 hover:ring-yellow-400"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setPreviewModal({ setPreviewModal({
@@ -617,25 +620,25 @@ export const HistoryPanel: React.FC = () => {
</div> </div>
); );
} else if (selectedEdit) { } else if (selectedEdit) {
const parentGen = dbGenerations.find(g => g.id === selectedEdit.parentGenerationId); const parentGen = filteredGenerations.find(g => g.id === selectedEdit.parentGenerationId) || dbGenerations.find(g => g.id === selectedEdit.parentGenerationId);
return ( return (
<div className="space-y-3"> <div className="space-y-3 p-3 bg-gray-50 rounded-lg">
<div className="space-y-2 text-xs text-gray-600"> <div className="space-y-2.5 text-xs text-gray-700">
<div> <div>
<span className="text-gray-500">:</span> <span className="text-gray-500">:</span>
<p className="text-gray-800 mt-1">{selectedEdit.instruction}</p> <p className="text-gray-800 mt-1 leading-relaxed">{selectedEdit.instruction}</p>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>:</span> <span className="text-gray-500">:</span>
<span></span> <span className="text-gray-700"></span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>:</span> <span className="text-gray-500">:</span>
<span>{new Date(selectedEdit.timestamp).toLocaleString()}</span> <span className="text-gray-700">{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 className="text-gray-500">:</span>
<span className="text-purple-600"></span> <span className="text-purple-600"></span>
</div> </div>
)} )}
@@ -643,24 +646,24 @@ export const HistoryPanel: React.FC = () => {
{/* 上传结果 */} {/* 上传结果 */}
{selectedEdit.uploadResults && selectedEdit.uploadResults.length > 0 && ( {selectedEdit.uploadResults && selectedEdit.uploadResults.length > 0 && (
<div> <div className="pt-2 border-t border-gray-200">
<h5 className="text-xs font-medium text-gray-500 mb-2"></h5> <h5 className="text-xs font-medium text-gray-500 mb-2"></h5>
<div className="space-y-1"> <div className="space-y-1.5">
{selectedEdit.uploadResults.map((result, index) => ( {selectedEdit.uploadResults.map((result, index) => (
<div key={index} className="text-xs"> <div key={index} className="text-xs">
<div className="flex justify-between"> <div className="flex justify-between">
<span> {index + 1}:</span> <span className="text-gray-500"> {index + 1}:</span>
<span className={result.success ? 'text-green-600' : 'text-red-600'}> <span className={result.success ? 'text-green-600' : 'text-red-600'}>
{result.success ? '成功' : '失败'} {result.success ? '成功' : '失败'}
</span> </span>
</div> </div>
{result.success && result.url && ( {result.success && result.url && (
<div className="text-blue-600 truncate"> <div className="text-blue-600 truncate text-xs mt-0.5">
{result.url.split('/').pop()} {result.url.split('/').pop()}
</div> </div>
)} )}
{result.error && ( {result.error && (
<div className="text-red-600 truncate"> <div className="text-red-600 truncate text-xs mt-0.5">
{result.error} {result.error}
</div> </div>
)} )}
@@ -672,7 +675,7 @@ export const HistoryPanel: React.FC = () => {
{/* 原始生成参考 */} {/* 原始生成参考 */}
{parentGen && ( {parentGen && (
<div> <div className="pt-2 border-t border-gray-200">
<h5 className="text-xs font-medium text-gray-500 mb-2"></h5> <h5 className="text-xs font-medium text-gray-500 mb-2"></h5>
<div className="text-xs text-gray-600"> <div className="text-xs text-gray-600">
基于: G{dbGenerations.findIndex(g => g.id === parentGen.id) + 1} 基于: G{dbGenerations.findIndex(g => g.id === parentGen.id) + 1}
@@ -687,7 +690,7 @@ export const HistoryPanel: React.FC = () => {
{parentGen.sourceAssets.slice(0, 4).map((asset: any, index: number) => ( {parentGen.sourceAssets.slice(0, 4).map((asset: any, index: number) => (
<div <div
key={asset.id} key={asset.id}
className="relative w-16 h-16 rounded border overflow-hidden cursor-pointer" className="relative w-16 h-16 rounded border overflow-hidden cursor-pointer hover:ring-2 hover:ring-yellow-400"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setPreviewModal({ setPreviewModal({
@@ -719,8 +722,8 @@ 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-3 text-center">
<p className="text-gray-500"></p> <p className="text-gray-400"></p>
</div> </div>
); );
} }
@@ -728,11 +731,11 @@ export const HistoryPanel: React.FC = () => {
</div> </div>
{/* 操作 */} {/* 操作 */}
<div className="space-y-3 flex-shrink-0"> <div className="space-y-2 flex-shrink-0 pt-2 border-t border-gray-100">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="w-full" className="w-full h-9 text-sm"
onClick={() => { onClick={() => {
// 查找当前显示的图像(生成记录或编辑记录) // 查找当前显示的图像(生成记录或编辑记录)
let imageUrl: string | null = null; let imageUrl: string | null = null;
@@ -775,7 +778,7 @@ export const HistoryPanel: React.FC = () => {
disabled={!selectedGenerationId && !useAppStore.getState().canvasImage} disabled={!selectedGenerationId && !useAppStore.getState().canvasImage}
> >
<Download className="h-4 w-4 mr-2" /> <Download className="h-4 w-4 mr-2" />
</Button> </Button>
</div> </div>

View File

@@ -188,29 +188,29 @@ export const ImageCanvas: React.FC = () => {
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* 工具栏 */} {/* 工具栏 */}
<div className="p-3 border-b border-gray-200 bg-white"> <div className="p-2 border-b border-gray-200 bg-white">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{/* 左侧 - 缩放控制 */} {/* 左侧 - 缩放控制 */}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-1">
<Button variant="outline" size="sm" onClick={() => handleZoom(-0.1)}> <Button variant="outline" size="sm" onClick={() => handleZoom(-0.1)} className="h-8 w-8 p-0">
<ZoomOut className="h-4 w-4" /> <ZoomOut className="h-4 w-4" />
</Button> </Button>
<span className="text-sm text-gray-400 min-w-[60px] text-center"> <span className="text-xs text-gray-400 min-w-[40px] text-center">
{Math.round(canvasZoom * 100)}% {Math.round(canvasZoom * 100)}%
</span> </span>
<Button variant="outline" size="sm" onClick={() => handleZoom(0.1)}> <Button variant="outline" size="sm" onClick={() => handleZoom(0.1)} className="h-8 w-8 p-0">
<ZoomIn className="h-4 w-4" /> <ZoomIn className="h-4 w-4" />
</Button> </Button>
<Button variant="outline" size="sm" onClick={handleReset}> <Button variant="outline" size="sm" onClick={handleReset} className="h-8 w-8 p-0">
<RotateCcw className="h-4 w-4" /> <RotateCcw className="h-4 w-4" />
</Button> </Button>
</div> </div>
{/* 右侧 - 工具和操作 */} {/* 右侧 - 工具和操作 */}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-1">
{selectedTool === 'mask' && ( {selectedTool === 'mask' && (
<> <>
<div className="flex items-center space-x-2 mr-2"> <div className="flex items-center space-x-1 mr-1">
<span className="text-xs text-gray-400">:</span> <span className="text-xs text-gray-400">:</span>
<input <input
type="range" type="range"
@@ -218,7 +218,7 @@ export const ImageCanvas: React.FC = () => {
max="50" max="50"
value={brushSize} value={brushSize}
onChange={(e) => setBrushSize(parseInt(e.target.value))} onChange={(e) => setBrushSize(parseInt(e.target.value))}
className="w-16 h-2 bg-gray-800 rounded-lg appearance-none cursor-pointer slider" className="w-12 h-2 bg-gray-800 rounded-lg appearance-none cursor-pointer slider"
/> />
<span className="text-xs text-gray-400 w-6">{brushSize}</span> <span className="text-xs text-gray-400 w-6">{brushSize}</span>
</div> </div>
@@ -227,6 +227,7 @@ export const ImageCanvas: React.FC = () => {
size="sm" size="sm"
onClick={clearBrushStrokes} onClick={clearBrushStrokes}
disabled={brushStrokes.length === 0} disabled={brushStrokes.length === 0}
className="h-8 w-8 p-0"
> >
<Eraser className="h-4 w-4" /> <Eraser className="h-4 w-4" />
</Button> </Button>
@@ -237,16 +238,14 @@ export const ImageCanvas: React.FC = () => {
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setShowMasks(!showMasks)} onClick={() => setShowMasks(!showMasks)}
className={cn(showMasks && 'bg-yellow-400/10 border-yellow-400/50')} className={cn(showMasks && 'bg-yellow-400/10 border-yellow-400/50', 'h-8 w-8 p-0')}
> >
{showMasks ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />} {showMasks ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
<span className="hidden sm:inline ml-2"></span>
</Button> </Button>
{canvasImage && ( {canvasImage && (
<Button variant="secondary" size="sm" onClick={handleDownload}> <Button variant="secondary" size="sm" onClick={handleDownload} className="h-8 px-2">
<Download className="h-4 w-4 mr-2" /> <Download className="h-4 w-4" />
<span className="hidden sm:inline"></span>
</Button> </Button>
)} )}
</div> </div>
@@ -260,14 +259,14 @@ export const ImageCanvas: React.FC = () => {
> >
{!image && !isGenerating && ( {!image && !isGenerating && (
<div className="absolute inset-0 flex items-center justify-center z-0"> <div className="absolute inset-0 flex items-center justify-center z-0">
<div className="text-center"> <div className="text-center max-w-xs">
<div className="text-6xl mb-4">🍌</div> <div className="text-5xl mb-3">🍌</div>
<h2 className="text-xl font-medium text-gray-300 mb-2"> <h2 className="text-lg font-medium text-gray-400 mb-1">
使 Nano Banana Nano Banana AI
</h2> </h2>
<p className="text-gray-500 max-w-md"> <p className="text-gray-500 text-sm">
{selectedTool === 'generate' {selectedTool === 'generate'
? '首先在提示框中描述您想要创建的内容' ? '在提示框中描述您想要创建的内容'
: '上传图像开始编辑' : '上传图像开始编辑'
} }
</p> </p>
@@ -276,10 +275,10 @@ export const ImageCanvas: React.FC = () => {
)} )}
{isGenerating && ( {isGenerating && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900/50 z-50"> <div className="absolute inset-0 flex items-center justify-center bg-gray-900/40 z-50 backdrop-blur-sm">
<div className="text-center"> <div className="text-center bg-white/90 rounded-xl p-6 shadow-lg">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-yellow-400 mb-4" /> <div className="animate-spin rounded-full h-10 w-10 border-2 border-yellow-400 border-t-transparent mx-auto mb-3" />
<p className="text-gray-300">...</p> <p className="text-gray-700 text-sm font-medium">...</p>
</div> </div>
</div> </div>
)} )}
@@ -353,29 +352,18 @@ export const ImageCanvas: React.FC = () => {
</div> </div>
{/* 状态栏 */} {/* 状态栏 */}
<div className="p-3 border-t border-gray-200 bg-white"> <div className="p-2 border-t border-gray-200 bg-white">
<div className="flex items-center justify-between text-xs text-gray-500"> <div className="flex items-center justify-between text-xs text-gray-500">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-2">
{brushStrokes.length > 0 && ( {brushStrokes.length > 0 && (
<span className="text-yellow-400">{brushStrokes.length} {brushStrokes.length !== 1 ? 's' : ''}</span> <span className="text-yellow-400">{brushStrokes.length} </span>
)} )}
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-1">
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-500">
© 2025 Mark Fulton - © 2025 Mark Fulton
<a
href="https://www.reinventing.ai/"
target="_blank"
rel="noopener noreferrer"
className="text-yellow-400 hover:text-yellow-300 transition-colors ml-1"
>
Reinventing.AI Solutions
</a>
</span> </span>
<span className="text-gray-600 hidden md:inline"></span>
<span className="text-yellow-400 hidden md:inline"></span>
<span className="hidden md:inline"> Gemini 2.5 Flash Image </span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -156,43 +156,46 @@ export const PromptComposer: React.FC = () => {
return ( return (
<> <>
<div className="w-80 lg:w-72 xl:w-80 h-full bg-white border-r border-gray-200 p-6 flex flex-col space-y-6 overflow-y-auto"> <div className="w-72 h-full bg-white border-r border-gray-200 p-4 flex flex-col space-y-5 overflow-y-auto">
<div> <div>
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-300"></h3> <h3 className="text-xs font-medium text-gray-500 uppercase tracking-wide"></h3>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-0.5">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => setShowHintsModal(true)} onClick={() => setShowHintsModal(true)}
className="h-6 w-6" className="h-6 w-6 text-gray-400 hover:text-gray-600 hover:bg-gray-100"
> >
<HelpCircle className="h-4 w-4" /> <HelpCircle className="h-3.5 w-3.5" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => setShowPromptPanel(false)} onClick={() => setShowPromptPanel(false)}
className="h-6 w-6" className="h-6 w-6 text-gray-400 hover:text-gray-600 hover:bg-gray-100"
title="隐藏提示面板" 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> </Button>
</div> </div>
</div> </div>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-1.5">
{tools.map((tool) => ( {tools.map((tool) => (
<button <button
key={tool.id} key={tool.id}
onClick={() => setSelectedTool(tool.id)} onClick={() => setSelectedTool(tool.id)}
className={cn( className={cn(
'flex flex-col items-center p-3 rounded-lg border transition-all duration-200', 'flex flex-col items-center p-2 rounded-lg border transition-all duration-200',
selectedTool === tool.id selectedTool === tool.id
? 'bg-yellow-400/10 border-yellow-400/50 text-yellow-400' ? 'bg-yellow-50 border-yellow-300 text-yellow-700 shadow-sm'
: 'bg-white border-yellow-400/50 text-gray-400 hover:bg-yellow-400 hover:text-white' : 'bg-gray-50 border-gray-200 text-gray-500 hover:bg-yellow-50 hover:border-yellow-300 hover:text-yellow-700'
)} )}
> >
<tool.icon className="h-5 w-5 mb-1" /> <tool.icon className="h-4 w-4 mb-1" />
<span className="text-xs font-medium">{tool.label}</span> <span className="text-xs font-medium">{tool.label}</span>
</button> </button>
))} ))}
@@ -206,7 +209,7 @@ export const PromptComposer: React.FC = () => {
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
className={cn( className={cn(
"border-2 border-dashed rounded-lg p-6 text-center transition-colors", "border border-dashed rounded-md p-4 text-center transition-colors",
isDragOver isDragOver
? "border-yellow-400 bg-yellow-400/10" ? "border-yellow-400 bg-yellow-400/10"
: "border-gray-300 hover:border-yellow-400" : "border-gray-300 hover:border-yellow-400"
@@ -216,13 +219,13 @@ export const PromptComposer: React.FC = () => {
{selectedTool === 'generate' ? '参考图像' : selectedTool === 'edit' ? '样式参考' : '上传图像'} {selectedTool === 'generate' ? '参考图像' : selectedTool === 'edit' ? '样式参考' : '上传图像'}
</label> </label>
{selectedTool === 'mask' && ( {selectedTool === 'mask' && (
<p className="text-xs text-gray-500 mb-3">使</p> <p className="text-xs text-gray-500 mb-2">使</p>
)} )}
{selectedTool === 'generate' && ( {selectedTool === 'generate' && (
<p className="text-xs text-gray-500 mb-3">2</p> <p className="text-xs text-gray-500 mb-2">2</p>
)} )}
{selectedTool === 'edit' && ( {selectedTool === 'edit' && (
<p className="text-xs text-gray-500 mb-3"> <p className="text-xs text-gray-500 mb-2">
{canvasImage ? '可选样式参考最多2张图像' : '上传要编辑的图像最多2张图像'} {canvasImage ? '可选样式参考最多2张图像' : '上传要编辑的图像最多2张图像'}
</p> </p>
)} )}
@@ -235,29 +238,30 @@ export const PromptComposer: React.FC = () => {
className="hidden" className="hidden"
/> />
<div className="flex flex-col items-center justify-center space-y-3"> <div className="flex flex-col items-center justify-center space-y-2">
<Upload className={cn("h-8 w-8", isDragOver ? "text-yellow-500" : "text-gray-400")} /> <Upload className={cn("h-6 w-6", isDragOver ? "text-yellow-500" : "text-gray-400")} />
<div> <div>
<p className={cn( <p className={cn(
"text-sm font-medium", "text-sm font-medium",
isDragOver ? "text-yellow-700" : "text-gray-600" isDragOver ? "text-yellow-700" : "text-gray-600"
)}> )}>
{isDragOver ? "释放文件以上传" : "拖拽图像到此处或点击上传"} {isDragOver ? "释放文件以上传" : "拖拽或点击上传"}
</p> </p>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
JPG, PNG, GIF JPG, PNG, GIF
</p> </p>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
className="mt-2" className="mt-1"
disabled={ disabled={
(selectedTool === 'generate' && uploadedImages.length >= 2) || (selectedTool === 'generate' && uploadedImages.length >= 2) ||
(selectedTool === 'edit' && editReferenceImages.length >= 2) (selectedTool === 'edit' && editReferenceImages.length >= 2)
} }
> >
<Upload className="h-4 w-4 mr-2" /> <Upload className="h-3 w-3 mr-1" />
</Button> </Button>
</div> </div>
@@ -266,13 +270,13 @@ export const PromptComposer: React.FC = () => {
{/* Show uploaded images preview */} {/* Show uploaded images preview */}
{((selectedTool === 'generate' && uploadedImages.length > 0) || {((selectedTool === 'generate' && uploadedImages.length > 0) ||
(selectedTool === 'edit' && editReferenceImages.length > 0)) && ( (selectedTool === 'edit' && editReferenceImages.length > 0)) && (
<div className="mt-3 space-y-2"> <div className="mt-2 space-y-2">
{(selectedTool === 'generate' ? uploadedImages : editReferenceImages).map((image, index) => ( {(selectedTool === 'generate' ? uploadedImages : editReferenceImages).map((image, index) => (
<div key={index} className="relative"> <div key={index} className="relative">
<img <img
src={image} src={image}
alt={`参考图像 ${index + 1}`} alt={`参考图像 ${index + 1}`}
className="w-full h-20 object-cover rounded-lg border border-gray-300" className="w-full h-16 object-cover rounded border border-gray-300"
/> />
<button <button
onClick={() => selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)} onClick={() => selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)}
@@ -280,7 +284,7 @@ export const PromptComposer: React.FC = () => {
> >
× ×
</button> </button>
<div className="absolute bottom-1 left-1 bg-gray-100/80 text-xs px-2 py-1 rounded text-gray-700"> <div className="absolute bottom-1 left-1 bg-gray-100/80 text-xs px-1 py-0.5 rounded text-gray-700">
{index + 1} {index + 1}
</div> </div>
</div> </div>
@@ -291,36 +295,36 @@ export const PromptComposer: React.FC = () => {
{/* 提示输入 */} {/* 提示输入 */}
<div> <div>
<label className="text-sm font-medium text-gray-700 mb-3 block"> <label className="text-xs font-medium text-gray-500 mb-2 block uppercase tracking-wide">
{selectedTool === 'generate' ? '描述您想要创建的内容' : '描述您的修改'} {selectedTool === 'generate' ? '提示词' : '编辑指令'}
</label> </label>
<Textarea <Textarea
value={currentPrompt} value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)} onChange={(e) => setCurrentPrompt(e.target.value)}
placeholder={ placeholder={
selectedTool === 'generate' selectedTool === 'generate'
? '宁静的山景日落,湖面倒映着金色的天空...' ? '描述您想要创建的内容...'
: '让天空更加戏剧化,添加暴风云...' : '描述您想要的修改...'
} }
className="min-h-[120px] resize-none" className="min-h-[100px] resize-none text-sm"
/> />
{/* 提示质量指示器 */} {/* 提示质量指示器 */}
<button <button
onClick={() => setShowHintsModal(true)} onClick={() => setShowHintsModal(true)}
className="mt-2 flex items-center text-xs hover:text-gray-600 transition-colors group" className="mt-2 flex items-center text-xs hover:text-gray-700 transition-colors group"
> >
{currentPrompt.length < 20 ? ( {currentPrompt.length < 20 ? (
<HelpCircle className="h-3 w-3 mr-2 text-red-500 group-hover:text-red-400" /> <HelpCircle className="h-3 w-3 mr-2 text-red-400 group-hover:text-red-500" />
) : ( ) : (
<div className={cn( <div className={cn(
'h-2 w-2 rounded-full mr-2', 'h-2 w-2 rounded-full mr-2',
currentPrompt.length < 50 ? 'bg-yellow-500' : 'bg-green-500' currentPrompt.length < 50 ? 'bg-yellow-400' : 'bg-green-400'
)} /> )} />
)} )}
<span className="text-gray-600 group-hover:text-gray-800"> <span className="text-gray-500 group-hover:text-gray-700">
{currentPrompt.length < 20 ? '添加更多细节以获得更好的结果' : {currentPrompt.length < 20 ? '添加更多细节' :
currentPrompt.length < 50 ? '细节水平良好' : '提示细节优秀'} currentPrompt.length < 50 ? '细节良好' : '细节优秀'}
</span> </span>
</button> </button>
</div> </div>
@@ -331,9 +335,9 @@ export const PromptComposer: React.FC = () => {
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
onClick={() => selectedTool === 'generate' ? cancelGeneration() : cancelEdit()} onClick={() => selectedTool === 'generate' ? cancelGeneration() : cancelEdit()}
className="flex-1 h-14 text-base font-medium bg-red-500 hover:bg-red-600" className="flex-1 h-12 text-sm font-medium bg-red-500 hover:bg-red-600 rounded-lg"
> >
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2" /> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
</Button> </Button>
</div> </div>
@@ -341,63 +345,30 @@ export const PromptComposer: React.FC = () => {
<Button <Button
onClick={handleGenerate} onClick={handleGenerate}
disabled={!currentPrompt.trim()} disabled={!currentPrompt.trim()}
className="w-full h-14 text-base font-medium" className="w-full h-12 text-sm font-medium rounded-lg shadow-sm hover:shadow-md transition-shadow"
> >
<Wand2 className="h-4 w-4 mr-2" /> <Wand2 className="h-4 w-4 mr-2" />
{selectedTool === 'generate' ? '生成' : '应用编辑'} {selectedTool === 'generate' ? '生成图像' : '应用编辑'}
</Button> </Button>
)} )}
{/* 高级控制 */} {/* 高级控制 */}
<div> <div className="pt-2 border-t border-gray-100">
<button <button
onClick={() => setShowAdvanced(!showAdvanced)} onClick={() => setShowAdvanced(!showAdvanced)}
className="flex items-center text-sm text-gray-600 hover:text-gray-800 transition-colors duration-200" className="flex items-center text-xs text-gray-500 hover:text-gray-700 transition-colors duration-200"
> >
{showAdvanced ? <ChevronDown className="h-4 w-4 mr-1" /> : <ChevronRight className="h-4 w-4 mr-1" />} {showAdvanced ? <ChevronDown className="h-3 w-3 mr-1" /> : <ChevronRight className="h-3 w-3 mr-1" />}
{showAdvanced ? '隐藏' : '显示'}
</button> </button>
<button
onClick={() => setShowClearConfirm(!showClearConfirm)}
className="flex items-center text-sm text-gray-600 hover:text-red-500 transition-colors duration-200 mt-2"
>
<RotateCcw className="h-4 w-4 mr-2" />
</button>
{showClearConfirm && (
<div className="mt-3 p-3 bg-gray-100 rounded-lg border border-gray-300">
<p className="text-xs text-gray-600 mb-3">
</p>
<div className="flex space-x-2">
<Button
variant="destructive"
size="sm"
onClick={handleClearSession}
className="flex-1"
>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setShowClearConfirm(false)}
className="flex-1"
>
</Button>
</div>
</div>
)}
{showAdvanced && ( {showAdvanced && (
<div className="mt-4 space-y-4"> <div className="mt-3 space-y-3">
{/* 创造力 */} {/* 创造力 */}
<div> <div>
<label className="text-xs text-gray-600 mb-2 block"> <label className="text-xs text-gray-500 mb-1.5 block flex justify-between">
({temperature}) <span></span>
<span className="font-mono">{temperature}</span>
</label> </label>
<input <input
type="range" type="range"
@@ -406,13 +377,13 @@ export const PromptComposer: React.FC = () => {
step="0.1" step="0.1"
value={temperature} value={temperature}
onChange={(e) => setTemperature(parseFloat(e.target.value))} onChange={(e) => setTemperature(parseFloat(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider" className="w-full h-1.5 bg-gray-200 rounded-full appearance-none cursor-pointer slider"
/> />
</div> </div>
{/* 种子 */} {/* 种子 */}
<div> <div>
<label className="text-xs text-gray-600 mb-2 block"> <label className="text-xs text-gray-500 mb-1.5 block">
() ()
</label> </label>
<input <input
@@ -420,36 +391,70 @@ export const PromptComposer: React.FC = () => {
value={seed || ''} value={seed || ''}
onChange={(e) => setSeed(e.target.value ? parseInt(e.target.value) : null)} onChange={(e) => setSeed(e.target.value ? parseInt(e.target.value) : null)}
placeholder="随机" placeholder="随机"
className="w-full h-8 px-2 bg-white border border-gray-300 rounded text-xs text-gray-900" className="w-full h-8 px-2.5 bg-gray-50 border border-gray-200 rounded-md text-xs text-gray-700 focus:outline-none focus:ring-1 focus:ring-yellow-400"
/> />
</div> </div>
</div> </div>
)} )}
<button
onClick={() => setShowClearConfirm(!showClearConfirm)}
className="flex items-center text-xs text-gray-500 hover:text-red-500 transition-colors duration-200 mt-3"
>
<RotateCcw className="h-3 w-3 mr-1.5" />
</button>
{showClearConfirm && (
<div className="mt-2 p-3 bg-red-50 rounded-lg border border-red-100">
<p className="text-xs text-red-700 mb-3">
</p>
<div className="flex space-x-2">
<Button
variant="destructive"
size="sm"
onClick={handleClearSession}
className="flex-1 h-8 text-xs"
>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setShowClearConfirm(false)}
className="flex-1 h-8 text-xs border-gray-200"
>
</Button>
</div>
</div>
)}
</div> </div>
{/* 键盘快捷键 */} {/* 键盘快捷键 */}
<div className="pt-4 border-t border-gray-200"> <div className="pt-3 border-t border-gray-100">
<h4 className="text-xs font-medium text-gray-600 mb-2"></h4> <h4 className="text-xs font-medium text-gray-500 mb-2 uppercase tracking-wide"></h4>
<div className="space-y-1 text-xs text-gray-700"> <div className="space-y-1.5 text-xs text-gray-600">
<div className="flex justify-between"> <div className="flex justify-between">
<span></span> <span className="text-gray-500"></span>
<span> + Enter</span> <span className="font-mono bg-gray-100 px-1.5 py-0.5 rounded text-gray-700"> + Enter</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span></span> <span className="text-gray-500"></span>
<span> + R</span> <span className="font-mono bg-gray-100 px-1.5 py-0.5 rounded text-gray-700"> + R</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span></span> <span className="text-gray-500"></span>
<span>E</span> <span className="font-mono bg-gray-100 px-1.5 py-0.5 rounded text-gray-700">E</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span></span> <span className="text-gray-500"></span>
<span>H</span> <span className="font-mono bg-gray-100 px-1.5 py-0.5 rounded text-gray-700">H</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span></span> <span className="text-gray-500"></span>
<span>P</span> <span className="font-mono bg-gray-100 px-1.5 py-0.5 rounded text-gray-700">P</span>
</div> </div>
</div> </div>
</div> </div>