初始化提交

This commit is contained in:
2025-09-14 02:05:42 +08:00
parent 1a3730454e
commit 9f94e92eaf
19 changed files with 385 additions and 322 deletions

View File

@@ -39,6 +39,7 @@ export const PromptComposer: React.FC = () => {
const [showAdvanced, setShowAdvanced] = useState(false);
const [showClearConfirm, setShowClearConfirm] = useState(false);
const [showHintsModal, setShowHintsModal] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleGenerate = () => {
@@ -60,39 +61,64 @@ export const PromptComposer: React.FC = () => {
}
};
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
const handleFileUpload = async (file: File) => {
if (file && file.type.startsWith('image/')) {
try {
const base64 = await blobToBase64(file);
const dataUrl = `data:${file.type};base64,${base64}`;
if (selectedTool === 'generate') {
// Add to reference images (max 2)
// 添加到参考图像最多2张
if (uploadedImages.length < 2) {
addUploadedImage(dataUrl);
}
} else if (selectedTool === 'edit') {
// For edit mode, add to separate edit reference images (max 2)
// 编辑模式下添加到单独的编辑参考图像最多2张
if (editReferenceImages.length < 2) {
addEditReferenceImage(dataUrl);
}
// Set as canvas image if none exists
// 如果没有画布图像,则设置为画布图像
if (!canvasImage) {
setCanvasImage(dataUrl);
}
} else if (selectedTool === 'mask') {
// For mask mode, set as canvas image immediately
// 遮罩模式下,立即设置为画布图像
clearUploadedImages();
addUploadedImage(dataUrl);
setCanvasImage(dataUrl);
}
} catch (error) {
console.error('Failed to upload image:', error);
console.error('上传图像失败:', error);
}
}
};
const handleFileInputChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
await handleFileUpload(file);
}
};
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(true);
};
const handleDragLeave = () => {
setIsDragOver(false);
};
const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(false);
const file = event.dataTransfer.files?.[0];
if (file) {
await handleFileUpload(file);
}
};
const handleClearSession = () => {
setCurrentPrompt('');
clearUploadedImages();
@@ -105,18 +131,18 @@ export const PromptComposer: React.FC = () => {
};
const tools = [
{ id: 'generate', icon: Wand2, label: 'Generate', description: 'Create from text' },
{ id: 'edit', icon: Edit3, label: 'Edit', description: 'Modify existing' },
{ id: 'mask', icon: MousePointer, label: 'Select', description: 'Click to select' },
{ id: 'generate', icon: Wand2, label: '生成', description: '从文本创建' },
{ id: 'edit', icon: Edit3, label: '编辑', description: '修改现有图像' },
{ id: 'mask', icon: MousePointer, label: '选择', description: '点击选择' },
] as const;
if (!showPromptPanel) {
return (
<div className="w-8 bg-gray-950 border-r border-gray-800 flex flex-col items-center justify-center">
<div className="w-8 bg-white border-r border-gray-200 flex flex-col items-center justify-center">
<button
onClick={() => setShowPromptPanel(true)}
className="w-6 h-16 bg-gray-800 hover:bg-gray-700 rounded-r-lg border border-l-0 border-gray-700 flex items-center justify-center transition-colors group"
title="Show Prompt Panel"
className="w-6 h-16 bg-gray-100 hover:bg-gray-200 rounded-r-lg border border-l-0 border-gray-300 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>
@@ -130,10 +156,10 @@ export const PromptComposer: React.FC = () => {
return (
<>
<div className="w-80 lg:w-72 xl:w-80 h-full bg-gray-950 border-r border-gray-800 p-6 flex flex-col space-y-6 overflow-y-auto">
<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>
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-gray-300">Mode</h3>
<h3 className="text-sm font-medium text-gray-300"></h3>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
@@ -148,7 +174,7 @@ export const PromptComposer: React.FC = () => {
size="icon"
onClick={() => setShowPromptPanel(false)}
className="h-6 w-6"
title="Hide Prompt Panel"
title="隐藏提示面板"
>
×
</Button>
@@ -163,7 +189,7 @@ export const PromptComposer: React.FC = () => {
'flex flex-col items-center p-3 rounded-lg border transition-all duration-200',
selectedTool === tool.id
? 'bg-yellow-400/10 border-yellow-400/50 text-yellow-400'
: 'bg-gray-900 border-gray-700 text-gray-400 hover:bg-gray-800 hover:text-gray-300'
: 'bg-white border-yellow-400/50 text-gray-400 hover:bg-yellow-400 hover:text-white'
)}
>
<tool.icon className="h-5 w-5 mb-1" />
@@ -173,42 +199,69 @@ export const PromptComposer: React.FC = () => {
</div>
</div>
{/* File Upload */}
{/* 文件上传 */}
<div>
<div>
<label className="text-sm font-medium text-gray-300 mb-1 block">
{selectedTool === 'generate' ? 'Reference Images' : selectedTool === 'edit' ? 'Style References' : 'Upload Image'}
</label>
{selectedTool === 'mask' && (
<p className="text-xs text-gray-400 mb-3">Edit an image with masks</p>
)}
{selectedTool === 'generate' && (
<p className="text-xs text-gray-500 mb-3">Optional, up to 2 images</p>
)}
{selectedTool === 'edit' && (
<p className="text-xs text-gray-500 mb-3">
{canvasImage ? 'Optional style references, up to 2 images' : 'Upload image to edit, up to 2 images'}
</p>
)}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileUpload}
className="hidden"
/>
<Button
variant="outline"
onClick={() => fileInputRef.current?.click()}
className="w-full"
disabled={
(selectedTool === 'generate' && uploadedImages.length >= 2) ||
(selectedTool === 'edit' && editReferenceImages.length >= 2)
}
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={cn(
"border-2 border-dashed rounded-lg p-6 text-center transition-colors",
isDragOver
? "border-yellow-400 bg-yellow-400/10"
: "border-gray-300 hover:border-yellow-400"
)}
>
<Upload className="h-4 w-4 mr-2" />
Upload
</Button>
<label className="text-sm font-medium text-gray-700 mb-1 block">
{selectedTool === 'generate' ? '参考图像' : selectedTool === 'edit' ? '样式参考' : '上传图像'}
</label>
{selectedTool === 'mask' && (
<p className="text-xs text-gray-500 mb-3">使</p>
)}
{selectedTool === 'generate' && (
<p className="text-xs text-gray-500 mb-3">2</p>
)}
{selectedTool === 'edit' && (
<p className="text-xs text-gray-500 mb-3">
{canvasImage ? '可选样式参考最多2张图像' : '上传要编辑的图像最多2张图像'}
</p>
)}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileInputChange}
className="hidden"
/>
<div className="flex flex-col items-center justify-center space-y-3">
<Upload className={cn("h-8 w-8", isDragOver ? "text-yellow-500" : "text-gray-400")} />
<div>
<p className={cn(
"text-sm font-medium",
isDragOver ? "text-yellow-700" : "text-gray-600"
)}>
{isDragOver ? "释放文件以上传" : "拖拽图像到此处或点击上传"}
</p>
<p className="text-xs text-gray-500 mt-1">
JPG, PNG, GIF
</p>
</div>
<Button
variant="outline"
onClick={() => fileInputRef.current?.click()}
className="mt-2"
disabled={
(selectedTool === 'generate' && uploadedImages.length >= 2) ||
(selectedTool === 'edit' && editReferenceImages.length >= 2)
}
>
<Upload className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* Show uploaded images preview */}
{((selectedTool === 'generate' && uploadedImages.length > 0) ||
@@ -218,45 +271,44 @@ export const PromptComposer: React.FC = () => {
<div key={index} className="relative">
<img
src={image}
alt={`Reference ${index + 1}`}
className="w-full h-20 object-cover rounded-lg border border-gray-700"
alt={`参考图像 ${index + 1}`}
className="w-full h-20 object-cover rounded-lg border border-gray-300"
/>
<button
onClick={() => selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)}
className="absolute top-1 right-1 bg-gray-900/80 text-gray-400 hover:text-gray-200 rounded-full p-1 transition-colors"
className="absolute top-1 right-1 bg-gray-100/80 text-gray-600 hover:text-gray-800 rounded-full p-1 transition-colors"
>
×
</button>
<div className="absolute bottom-1 left-1 bg-gray-900/80 text-xs px-2 py-1 rounded text-gray-300">
Ref {index + 1}
<div className="absolute bottom-1 left-1 bg-gray-100/80 text-xs px-2 py-1 rounded text-gray-700">
{index + 1}
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Prompt Input */}
{/* 提示输入 */}
<div>
<label className="text-sm font-medium text-gray-300 mb-3 block">
{selectedTool === 'generate' ? 'Describe what you want to create' : 'Describe your changes'}
<label className="text-sm font-medium text-gray-700 mb-3 block">
{selectedTool === 'generate' ? '描述您想要创建的内容' : '描述您的修改'}
</label>
<Textarea
value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)}
placeholder={
selectedTool === 'generate'
? 'A serene mountain landscape at sunset with a lake reflecting the golden sky...'
: 'Make the sky more dramatic, add storm clouds...'
? '宁静的山景日落,湖面倒映着金色的天空...'
: '让天空更加戏剧化,添加暴风云...'
}
className="min-h-[120px] resize-none"
/>
{/* Prompt Quality Indicator */}
{/* 提示质量指示器 */}
<button
onClick={() => setShowHintsModal(true)}
className="mt-2 flex items-center text-xs hover:text-gray-400 transition-colors group"
className="mt-2 flex items-center text-xs hover:text-gray-600 transition-colors group"
>
{currentPrompt.length < 20 ? (
<HelpCircle className="h-3 w-3 mr-2 text-red-500 group-hover:text-red-400" />
@@ -266,15 +318,15 @@ export const PromptComposer: React.FC = () => {
currentPrompt.length < 50 ? 'bg-yellow-500' : 'bg-green-500'
)} />
)}
<span className="text-gray-500 group-hover:text-gray-400">
{currentPrompt.length < 20 ? 'Add detail for better results' :
currentPrompt.length < 50 ? 'Good detail level' : 'Excellent prompt detail'}
<span className="text-gray-600 group-hover:text-gray-800">
{currentPrompt.length < 20 ? '添加更多细节以获得更好的结果' :
currentPrompt.length < 50 ? '细节水平良好' : '提示细节优秀'}
</span>
</button>
</div>
{/* Generate Button */}
{/* 生成按钮 */}
<Button
onClick={handleGenerate}
disabled={isGenerating || !currentPrompt.trim()}
@@ -283,38 +335,38 @@ export const PromptComposer: React.FC = () => {
{isGenerating ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2" />
Generating...
...
</>
) : (
<>
<Wand2 className="h-4 w-4 mr-2" />
{selectedTool === 'generate' ? 'Generate' : 'Apply Edit'}
{selectedTool === 'generate' ? '生成' : '应用编辑'}
</>
)}
</Button>
{/* Advanced Controls */}
{/* 高级控制 */}
<div>
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className="flex items-center text-sm text-gray-400 hover:text-gray-300 transition-colors duration-200"
className="flex items-center text-sm text-gray-600 hover:text-gray-800 transition-colors duration-200"
>
{showAdvanced ? <ChevronDown className="h-4 w-4 mr-1" /> : <ChevronRight className="h-4 w-4 mr-1" />}
{showAdvanced ? 'Hide' : 'Show'} Advanced Controls
{showAdvanced ? '隐藏' : '显示'}
</button>
<button
onClick={() => setShowClearConfirm(!showClearConfirm)}
className="flex items-center text-sm text-gray-400 hover:text-red-400 transition-colors duration-200 mt-2"
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" />
Clear Session
</button>
{showClearConfirm && (
<div className="mt-3 p-3 bg-gray-800 rounded-lg border border-gray-700">
<p className="text-xs text-gray-300 mb-3">
Are you sure you want to clear this session? This will remove all uploads, prompts, and canvas content.
<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
@@ -323,7 +375,7 @@ export const PromptComposer: React.FC = () => {
onClick={handleClearSession}
className="flex-1"
>
Yes, Clear
</Button>
<Button
variant="outline"
@@ -331,7 +383,7 @@ export const PromptComposer: React.FC = () => {
onClick={() => setShowClearConfirm(false)}
className="flex-1"
>
Cancel
</Button>
</div>
</div>
@@ -339,10 +391,10 @@ export const PromptComposer: React.FC = () => {
{showAdvanced && (
<div className="mt-4 space-y-4">
{/* Temperature */}
{/* 创造力 */}
<div>
<label className="text-xs text-gray-400 mb-2 block">
Creativity ({temperature})
<label className="text-xs text-gray-600 mb-2 block">
({temperature})
</label>
<input
type="range"
@@ -351,55 +403,55 @@ export const PromptComposer: React.FC = () => {
step="0.1"
value={temperature}
onChange={(e) => setTemperature(parseFloat(e.target.value))}
className="w-full h-2 bg-gray-800 rounded-lg appearance-none cursor-pointer slider"
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
/>
</div>
{/* Seed */}
{/* 种子 */}
<div>
<label className="text-xs text-gray-400 mb-2 block">
Seed (optional)
<label className="text-xs text-gray-600 mb-2 block">
()
</label>
<input
type="number"
value={seed || ''}
onChange={(e) => setSeed(e.target.value ? parseInt(e.target.value) : null)}
placeholder="Random"
className="w-full h-8 px-2 bg-gray-900 border border-gray-700 rounded text-xs text-gray-100"
placeholder="随机"
className="w-full h-8 px-2 bg-white border border-gray-300 rounded text-xs text-gray-900"
/>
</div>
</div>
)}
</div>
{/* Keyboard Shortcuts */}
<div className="pt-4 border-t border-gray-800">
<h4 className="text-xs font-medium text-gray-400 mb-2">Shortcuts</h4>
<div className="space-y-1 text-xs text-gray-500">
{/* 键盘快捷键 */}
<div className="pt-4 border-t border-gray-200">
<h4 className="text-xs font-medium text-gray-600 mb-2"></h4>
<div className="space-y-1 text-xs text-gray-700">
<div className="flex justify-between">
<span>Generate</span>
<span></span>
<span> + Enter</span>
</div>
<div className="flex justify-between">
<span>Re-roll</span>
<span></span>
<span> + R</span>
</div>
<div className="flex justify-between">
<span>Edit mode</span>
<span></span>
<span>E</span>
</div>
<div className="flex justify-between">
<span>History</span>
<span></span>
<span>H</span>
</div>
<div className="flex justify-between">
<span>Toggle Panel</span>
<span></span>
<span>P</span>
</div>
</div>
</div>
</div>
{/* Prompt Hints Modal */}
{/* 提示提示模态框 */}
<PromptHints open={showHintsModal} onOpenChange={setShowHintsModal} />
</>
);