fix: lock session intake after creation
This commit is contained in:
@@ -344,6 +344,7 @@ export default function Home() {
|
||||
|
||||
<div className="space-y-8">
|
||||
<PromptPanel
|
||||
session={current}
|
||||
onGenerate={handleGenerate}
|
||||
onUploadProject={handleUploadProject}
|
||||
loading={loading}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
import type { GenSession, ProjectInputMode, UploadedImage } from '@/lib/types';
|
||||
|
||||
const PRESET_STYLES = [
|
||||
{ id: 'plush', label: '毛绒玩偶' },
|
||||
@@ -17,6 +18,7 @@ type UploadProjectFile = {
|
||||
};
|
||||
|
||||
export type PromptPanelProps = {
|
||||
session: GenSession | null;
|
||||
onGenerate: (opts: { prompt: string; refImages: string[]; count: number; style?: string }) => void;
|
||||
onUploadProject: (opts: {
|
||||
mode: 'remix' | 'replicate';
|
||||
@@ -35,7 +37,90 @@ function filePreview(file: File | null) {
|
||||
return file ? URL.createObjectURL(file) : '';
|
||||
}
|
||||
|
||||
export default function PromptPanel({ onGenerate, onUploadProject, loading, uploadLoading }: PromptPanelProps) {
|
||||
function tabFromMode(mode?: ProjectInputMode): Tab {
|
||||
if (mode === 'remix') return 'remix';
|
||||
if (mode === 'replicate' || mode === 'extend') return 'replicate';
|
||||
return 'idea';
|
||||
}
|
||||
|
||||
function modeLabel(mode?: ProjectInputMode) {
|
||||
if (mode === 'remix') return '二创';
|
||||
if (mode === 'replicate') return '复刻';
|
||||
if (mode === 'extend') return '复制';
|
||||
return '想法';
|
||||
}
|
||||
|
||||
function imageSourcesForSession(session: GenSession) {
|
||||
const uploaded = session.uploadedImages?.map(image => image.url) ?? [];
|
||||
return uploaded.length ? uploaded : session.refImages;
|
||||
}
|
||||
|
||||
function uploadedLabel(image: UploadedImage) {
|
||||
if (image.role === 'subject') return '主体图';
|
||||
if (image.role === 'reference') return '参考图';
|
||||
return image.accessoryName || image.role;
|
||||
}
|
||||
|
||||
function LockedSessionInput({ session }: { session: GenSession }) {
|
||||
const images = imageSourcesForSession(session);
|
||||
return (
|
||||
<div className="rounded-[8px] border border-[#e6f578]/18 bg-[#e6f578]/[0.035] p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[10px] uppercase tracking-[0.18em] text-[#e6f578]/65">Locked session input</div>
|
||||
<div className="mt-1 text-sm font-semibold text-white">本会话入口已锁定 · {modeLabel(session.inputMode)}</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-black/35 px-3 py-1 text-[11px] text-white/45">
|
||||
要换内容,请从左侧新建会话
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-[minmax(0,1fr)_220px]">
|
||||
<div className="rounded-[8px] border border-white/10 bg-black/28 p-3">
|
||||
<div className="mb-1.5 text-[10px] uppercase tracking-[0.14em] text-white/38">Prompt</div>
|
||||
<p className="text-[13px] leading-relaxed text-white/76">{session.prompt || '未填写'}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-[11px] text-white/54">
|
||||
<div className="rounded-[8px] border border-white/10 bg-black/24 p-3">
|
||||
<div className="text-white/32">数量</div>
|
||||
<div className="mt-1 font-semibold text-white">{session.count} 张</div>
|
||||
</div>
|
||||
<div className="rounded-[8px] border border-white/10 bg-black/24 p-3">
|
||||
<div className="text-white/32">创建时间</div>
|
||||
<div className="mt-1 font-semibold text-white">
|
||||
{new Date(session.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<div className="mb-2 text-[10px] uppercase tracking-[0.14em] text-white/38">Reference</div>
|
||||
{images.length ? (
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{images.slice(0, 6).map((url, index) => {
|
||||
const uploaded = session.uploadedImages?.find(image => image.url === url);
|
||||
return (
|
||||
<div key={`${url}-${index}`} className="relative h-20 w-20 overflow-hidden rounded-[8px] ring-1 ring-white/[0.1]">
|
||||
<img src={url} alt={uploaded ? uploadedLabel(uploaded) : `参考图 ${index + 1}`} className="h-full w-full object-cover" />
|
||||
<div className="absolute bottom-1 left-1 rounded bg-black/70 px-1.5 py-0.5 text-[9px] text-white/70">
|
||||
{uploaded ? uploadedLabel(uploaded) : `参考 ${index + 1}`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-[8px] border border-dashed border-white/10 bg-black/18 px-3 py-4 text-xs text-white/34">
|
||||
本会话没有参考图。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PromptPanel({ session, onGenerate, onUploadProject, loading, uploadLoading }: PromptPanelProps) {
|
||||
const [tab, setTab] = useState<Tab>('idea');
|
||||
const [prompt, setPrompt] = useState('AI 毛绒陪伴玩具,机甲头盔,胸前挂 M logo,橙白配色,圆胖体型');
|
||||
const [refs, setRefs] = useState<string[]>([]);
|
||||
@@ -86,6 +171,8 @@ export default function PromptPanel({ onGenerate, onUploadProject, loading, uplo
|
||||
|
||||
const busy = loading || uploadLoading;
|
||||
const replicatePreview = filePreview(replicateFile);
|
||||
const locked = Boolean(session);
|
||||
const activeTab = session ? tabFromMode(session.inputMode) : tab;
|
||||
|
||||
return (
|
||||
<section className="card p-7 space-y-6">
|
||||
@@ -102,8 +189,10 @@ export default function PromptPanel({ onGenerate, onUploadProject, loading, uplo
|
||||
].map(([id, label]) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => setTab(id as Tab)}
|
||||
className={`seg-item ${tab === id ? 'seg-item-active' : ''}`}
|
||||
onClick={() => { if (!locked) setTab(id as Tab); }}
|
||||
disabled={locked}
|
||||
title={locked ? '本会话入口已锁定;从左侧新建会话后可重新选择' : undefined}
|
||||
className={`seg-item ${activeTab === id ? 'seg-item-active' : ''} ${locked ? 'cursor-not-allowed opacity-70' : ''}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
@@ -111,7 +200,9 @@ export default function PromptPanel({ onGenerate, onUploadProject, loading, uplo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tab === 'idea' && (
|
||||
{session ? (
|
||||
<LockedSessionInput session={session} />
|
||||
) : activeTab === 'idea' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5">
|
||||
@@ -163,7 +254,7 @@ export default function PromptPanel({ onGenerate, onUploadProject, loading, uplo
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === 'remix' && (
|
||||
{!session && activeTab === 'remix' && (
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5">
|
||||
@@ -212,7 +303,7 @@ export default function PromptPanel({ onGenerate, onUploadProject, loading, uplo
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'replicate' && (
|
||||
{!session && activeTab === 'replicate' && (
|
||||
<div className="grid md:grid-cols-[220px_1fr] gap-5 items-start">
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5">
|
||||
@@ -256,7 +347,7 @@ export default function PromptPanel({ onGenerate, onUploadProject, loading, uplo
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(tab === 'idea' || tab === 'remix') && (
|
||||
{!session && (activeTab === 'idea' || activeTab === 'remix') && (
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5">
|
||||
风格
|
||||
@@ -277,8 +368,8 @@ export default function PromptPanel({ onGenerate, onUploadProject, loading, uplo
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-end justify-between gap-4 pt-2">
|
||||
{(tab === 'idea' || tab === 'remix') ? (
|
||||
{!session && <div className="flex items-end justify-between gap-4 pt-2">
|
||||
{(activeTab === 'idea' || activeTab === 'remix') ? (
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5">
|
||||
数量
|
||||
@@ -295,12 +386,12 @@ export default function PromptPanel({ onGenerate, onUploadProject, loading, uplo
|
||||
</div>
|
||||
) : <div />}
|
||||
<button
|
||||
onClick={tab === 'idea' ? submitIdea : tab === 'remix' ? submitRemix : submitReplicate}
|
||||
onClick={activeTab === 'idea' ? submitIdea : activeTab === 'remix' ? submitRemix : submitReplicate}
|
||||
disabled={
|
||||
busy
|
||||
|| (tab === 'idea' && !prompt.trim())
|
||||
|| (tab === 'remix' && remixFiles.length === 0)
|
||||
|| (tab === 'replicate' && !replicateFile)
|
||||
|| (activeTab === 'idea' && !prompt.trim())
|
||||
|| (activeTab === 'remix' && remixFiles.length === 0)
|
||||
|| (activeTab === 'replicate' && !replicateFile)
|
||||
}
|
||||
className="btn btn-primary px-5 py-2.5 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
@@ -313,14 +404,14 @@ export default function PromptPanel({ onGenerate, onUploadProject, loading, uplo
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{tab === 'idea' ? '批量生成' : tab === 'remix' ? '生成变体' : '复刻并锁定'}
|
||||
{activeTab === 'idea' ? '批量生成' : activeTab === 'remix' ? '生成变体' : '复刻并锁定'}
|
||||
<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>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function Sidebar({
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M12 5v14M5 12h14" strokeLinecap="round" />
|
||||
</svg>
|
||||
新会话
|
||||
新会话 · 重新选择
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user