feat: improve AI layer editing UI, enforce aspect ratio matching through system prompt constraints
Build and Deploy / build-and-push (push) Successful in 57s
Build and Deploy / build-and-push (push) Successful in 57s
This commit is contained in:
+28
-22
@@ -69,7 +69,11 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
|||||||
ctx.drawImage(layer.img, 0, 0);
|
ctx.drawImage(layer.img, 0, 0);
|
||||||
const base64 = tempCanvas.toDataURL('image/png');
|
const base64 = tempCanvas.toDataURL('image/png');
|
||||||
|
|
||||||
const newUrl = await editUVLayer(aiPrompt, base64, token || '');
|
const fullPrompt = `The user has provided an image layer. Please apply the following edit exactly as requested, while strictly maintaining the EXACT same aspect ratio, resolution, and general structure of the original image.
|
||||||
|
User Prompt: "${aiPrompt}"
|
||||||
|
Negative constraints: Do NOT change the aspect ratio. Do NOT add completely unrelated elements. Do NOT crop the image boundaries.`;
|
||||||
|
|
||||||
|
const newUrl = await editUVLayer(fullPrompt, base64, token || '');
|
||||||
|
|
||||||
if (!originalLayerUrl && !regenerate) {
|
if (!originalLayerUrl && !regenerate) {
|
||||||
setOriginalLayerUrl(layer.url);
|
setOriginalLayerUrl(layer.url);
|
||||||
@@ -485,43 +489,45 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
|||||||
{/* AI Layer Edit */}
|
{/* AI Layer Edit */}
|
||||||
<div className="h-px bg-zinc-700 my-1" />
|
<div className="h-px bg-zinc-700 my-1" />
|
||||||
<p className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest flex items-center justify-between">
|
<p className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest flex items-center justify-between">
|
||||||
AI Texture Edit
|
<span className="flex items-center gap-1.5"><span className="text-blue-400">✨</span> Edit with AI</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 relative">
|
||||||
|
{isEditingLayer && (
|
||||||
|
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-zinc-900/80 backdrop-blur-sm rounded border border-zinc-700 p-2 text-center text-xs text-blue-400">
|
||||||
|
<Loader2 size={18} className="animate-spin mb-1" />
|
||||||
|
Generating...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<textarea
|
||||||
|
placeholder="e.g. Change the color to red, add a vintage filter..."
|
||||||
|
value={aiPrompt}
|
||||||
|
onChange={e => setAiPrompt(e.target.value)}
|
||||||
|
className="w-full h-16 bg-zinc-900 border border-zinc-700 rounded p-2 text-xs text-zinc-300 placeholder:text-zinc-600 resize-none outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
{aiPreviewUrl ? (
|
{aiPreviewUrl ? (
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<p className="text-[10px] text-zinc-400">Review AI Generation:</p>
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button onClick={handleApproveEdit} className="flex-1 bg-green-500 hover:bg-green-600 text-white py-1.5 rounded text-xs font-semibold">
|
<button onClick={handleApproveEdit} className="flex-1 py-1.5 bg-green-500/20 text-green-400 hover:bg-green-500/30 rounded text-[11px] font-semibold border border-green-500/30 transition-colors">
|
||||||
Approve
|
Approve
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleRejectEdit} className="flex-1 bg-zinc-700 hover:bg-zinc-600 text-white py-1.5 rounded text-xs font-semibold">
|
<button onClick={() => handleAiEdit(true)} className="flex-1 py-1.5 bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 rounded text-[11px] font-semibold border border-blue-500/30 transition-colors">
|
||||||
Reject
|
Regenerate
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button onClick={handleRejectEdit} className="flex-1 py-1.5 bg-red-500/20 text-red-400 hover:bg-red-500/30 rounded text-[11px] font-semibold border border-red-500/30 transition-colors">
|
||||||
<button onClick={() => handleAiEdit(true)} disabled={isEditingLayer} className="w-full bg-blue-500/20 hover:bg-blue-500/30 text-blue-400 py-1.5 rounded text-xs font-semibold flex items-center justify-center gap-2">
|
Revert
|
||||||
{isEditingLayer ? <Loader2 size={12} className="animate-spin" /> : null} Regenerate
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<textarea
|
|
||||||
value={aiPrompt}
|
|
||||||
onChange={e => setAiPrompt(e.target.value)}
|
|
||||||
placeholder="e.g. Turn into a rusty metal texture"
|
|
||||||
className="w-full bg-zinc-900 border border-zinc-700 rounded p-2 text-xs text-zinc-200 placeholder-zinc-600 resize-none h-16 outline-none focus:border-blue-500"
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAiEdit(false)}
|
onClick={() => handleAiEdit(false)}
|
||||||
disabled={isEditingLayer || !aiPrompt.trim()}
|
disabled={!aiPrompt.trim()}
|
||||||
className="w-full bg-blue-500 hover:bg-blue-600 disabled:opacity-50 text-white py-1.5 rounded text-xs font-semibold flex items-center justify-center gap-2 transition-colors"
|
className="w-full py-1.5 bg-blue-500/10 hover:bg-blue-500/20 disabled:opacity-50 disabled:cursor-not-allowed rounded text-[11px] font-semibold text-blue-400 border border-blue-500/30 transition-colors flex items-center justify-center gap-1.5"
|
||||||
>
|
>
|
||||||
{isEditingLayer ? <Loader2 size={14} className="animate-spin" /> : null}
|
<span className="text-[14px] leading-none">✨</span> Generate Edit
|
||||||
Generate Edit
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user