Files
ai-toy-patent-workflow/src/components/PromptPanel.tsx

149 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useRef, useState } from 'react';
const PRESET_STYLES = [
{ id: 'plush', label: '毛绒玩偶' },
{ id: 'mecha', label: '机甲风' },
{ id: 'kawaii', label: '可爱萌系' },
{ id: 'blueprint', label: '专利蓝图' },
{ id: 'cyber', label: '赛博朋克' },
{ id: 'minimal', label: '极简' },
];
export type PromptPanelProps = {
onGenerate: (opts: { prompt: string; refImages: string[]; count: number; style?: string }) => void;
loading: boolean;
};
export default function PromptPanel({ onGenerate, loading }: PromptPanelProps) {
const [prompt, setPrompt] = useState('AI 毛绒陪伴玩具,机甲头盔,胸前挂 M logo橙白配色圆胖体型');
const [refs, setRefs] = useState<string[]>([]);
const [count, setCount] = useState(8);
const [style, setStyle] = useState<string>('');
const fileInput = useRef<HTMLInputElement>(null);
function handleFiles(files: FileList | null) {
if (!files) return;
Array.from(files).slice(0, 4 - refs.length).forEach(f => {
const r = new FileReader();
r.onload = () => setRefs(prev => [...prev, r.result as string]);
r.readAsDataURL(f);
});
}
function submit() {
if (!prompt.trim() || loading) return;
onGenerate({ prompt: prompt.trim(), refImages: refs, count, style: style || undefined });
}
return (
<div className="card p-7 space-y-6">
<div>
<label className="block text-xs font-medium text-zinc-500 uppercase tracking-wider mb-2.5">
Prompt
</label>
<textarea
value={prompt}
onChange={e => setPrompt(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) submit(); }}
rows={3}
placeholder="描述要生成的玩具意向…"
className="field text-[15px] leading-relaxed"
/>
<p className="mt-2 text-[11px] text-zinc-400 flex items-center gap-1.5">
<kbd className="kbd"></kbd><kbd className="kbd"></kbd>
</p>
</div>
<div>
<label className="block text-xs font-medium text-zinc-500 uppercase tracking-wider mb-2.5">
<span className="text-zinc-400 normal-case tracking-normal">· 4 </span>
</label>
<div className="flex flex-wrap gap-2.5">
{refs.map((r, i) => (
<div key={i} className="relative w-20 h-20 rounded-xl overflow-hidden ring-1 ring-zinc-200 group">
<img src={r} alt="ref" className="w-full h-full object-cover" />
<button
onClick={() => setRefs(prev => prev.filter((_, j) => j !== i))}
className="absolute top-1 right-1 w-5 h-5 rounded-full bg-zinc-900 text-white text-[10px] opacity-0 group-hover:opacity-100 transition-opacity shadow-md"
></button>
</div>
))}
{refs.length < 4 && (
<button
onClick={() => fileInput.current?.click()}
className="w-20 h-20 rounded-xl border-2 border-dashed border-zinc-200 hover:border-zinc-400 hover:bg-zinc-50 text-zinc-400 hover:text-zinc-600 text-2xl transition-colors flex items-center justify-center"
>+</button>
)}
<input
ref={fileInput}
type="file"
accept="image/*"
multiple
hidden
onChange={e => handleFiles(e.target.files)}
/>
</div>
</div>
<div>
<label className="block text-xs font-medium text-zinc-500 uppercase tracking-wider mb-2.5">
</label>
<div className="flex flex-wrap gap-1.5">
<button
onClick={() => setStyle('')}
className={style === '' ? 'btn btn-primary text-xs px-3 py-1.5' : 'btn btn-outline text-xs px-3 py-1.5'}
></button>
{PRESET_STYLES.map(s => (
<button
key={s.id}
onClick={() => setStyle(s.label)}
className={style === s.label ? 'btn btn-primary text-xs px-3 py-1.5' : 'btn btn-outline text-xs px-3 py-1.5'}
>{s.label}</button>
))}
</div>
</div>
<div className="flex items-end justify-between gap-4 pt-2">
<div>
<label className="block text-xs font-medium text-zinc-500 uppercase tracking-wider mb-2.5">
</label>
<div className="seg">
{[4, 8, 12].map(n => (
<button
key={n}
onClick={() => setCount(n)}
className={`seg-item ${count === n ? 'seg-item-active' : ''}`}
>{n} </button>
))}
</div>
</div>
<button
onClick={submit}
disabled={loading || !prompt.trim()}
className="btn btn-primary px-5 py-2.5 disabled:opacity-40 disabled:cursor-not-allowed"
>
{loading ? (
<>
<svg width="14" height="14" viewBox="0 0 24 24" className="animate-spin" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
</svg>
</>
) : (
<>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M5 12h14M13 5l7 7-7 7" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</>
)}
</button>
</div>
</div>
);
}