fix: lock session intake after creation

This commit is contained in:
2026-05-19 16:17:58 +08:00
parent 193708a836
commit c232dd0526
3 changed files with 108 additions and 16 deletions

View File

@@ -344,6 +344,7 @@ export default function Home() {
<div className="space-y-8"> <div className="space-y-8">
<PromptPanel <PromptPanel
session={current}
onGenerate={handleGenerate} onGenerate={handleGenerate}
onUploadProject={handleUploadProject} onUploadProject={handleUploadProject}
loading={loading} loading={loading}

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import type { GenSession, ProjectInputMode, UploadedImage } from '@/lib/types';
const PRESET_STYLES = [ const PRESET_STYLES = [
{ id: 'plush', label: '毛绒玩偶' }, { id: 'plush', label: '毛绒玩偶' },
@@ -17,6 +18,7 @@ type UploadProjectFile = {
}; };
export type PromptPanelProps = { export type PromptPanelProps = {
session: GenSession | null;
onGenerate: (opts: { prompt: string; refImages: string[]; count: number; style?: string }) => void; onGenerate: (opts: { prompt: string; refImages: string[]; count: number; style?: string }) => void;
onUploadProject: (opts: { onUploadProject: (opts: {
mode: 'remix' | 'replicate'; mode: 'remix' | 'replicate';
@@ -35,7 +37,90 @@ function filePreview(file: File | null) {
return file ? URL.createObjectURL(file) : ''; 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 [tab, setTab] = useState<Tab>('idea');
const [prompt, setPrompt] = useState('AI 毛绒陪伴玩具,机甲头盔,胸前挂 M logo橙白配色圆胖体型'); const [prompt, setPrompt] = useState('AI 毛绒陪伴玩具,机甲头盔,胸前挂 M logo橙白配色圆胖体型');
const [refs, setRefs] = useState<string[]>([]); const [refs, setRefs] = useState<string[]>([]);
@@ -86,6 +171,8 @@ export default function PromptPanel({ onGenerate, onUploadProject, loading, uplo
const busy = loading || uploadLoading; const busy = loading || uploadLoading;
const replicatePreview = filePreview(replicateFile); const replicatePreview = filePreview(replicateFile);
const locked = Boolean(session);
const activeTab = session ? tabFromMode(session.inputMode) : tab;
return ( return (
<section className="card p-7 space-y-6"> <section className="card p-7 space-y-6">
@@ -102,8 +189,10 @@ export default function PromptPanel({ onGenerate, onUploadProject, loading, uplo
].map(([id, label]) => ( ].map(([id, label]) => (
<button <button
key={id} key={id}
onClick={() => setTab(id as Tab)} onClick={() => { if (!locked) setTab(id as Tab); }}
className={`seg-item ${tab === id ? 'seg-item-active' : ''}`} disabled={locked}
title={locked ? '本会话入口已锁定;从左侧新建会话后可重新选择' : undefined}
className={`seg-item ${activeTab === id ? 'seg-item-active' : ''} ${locked ? 'cursor-not-allowed opacity-70' : ''}`}
> >
{label} {label}
</button> </button>
@@ -111,7 +200,9 @@ export default function PromptPanel({ onGenerate, onUploadProject, loading, uplo
</div> </div>
</div> </div>
{tab === 'idea' && ( {session ? (
<LockedSessionInput session={session} />
) : activeTab === 'idea' && (
<> <>
<div> <div>
<label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5"> <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 className="space-y-5">
<div> <div>
<label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5"> <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> </div>
)} )}
{tab === 'replicate' && ( {!session && activeTab === 'replicate' && (
<div className="grid md:grid-cols-[220px_1fr] gap-5 items-start"> <div className="grid md:grid-cols-[220px_1fr] gap-5 items-start">
<div> <div>
<label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5"> <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> </div>
)} )}
{(tab === 'idea' || tab === 'remix') && ( {!session && (activeTab === 'idea' || activeTab === 'remix') && (
<div> <div>
<label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5"> <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>
)} )}
<div className="flex items-end justify-between gap-4 pt-2"> {!session && <div className="flex items-end justify-between gap-4 pt-2">
{(tab === 'idea' || tab === 'remix') ? ( {(activeTab === 'idea' || activeTab === 'remix') ? (
<div> <div>
<label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5"> <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>
) : <div />} ) : <div />}
<button <button
onClick={tab === 'idea' ? submitIdea : tab === 'remix' ? submitRemix : submitReplicate} onClick={activeTab === 'idea' ? submitIdea : activeTab === 'remix' ? submitRemix : submitReplicate}
disabled={ disabled={
busy busy
|| (tab === 'idea' && !prompt.trim()) || (activeTab === 'idea' && !prompt.trim())
|| (tab === 'remix' && remixFiles.length === 0) || (activeTab === 'remix' && remixFiles.length === 0)
|| (tab === 'replicate' && !replicateFile) || (activeTab === 'replicate' && !replicateFile)
} }
className="btn btn-primary px-5 py-2.5 disabled:opacity-40 disabled:cursor-not-allowed" 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"> <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" /> <path d="M5 12h14M13 5l7 7-7 7" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</> </>
)} )}
</button> </button>
</div> </div>}
</section> </section>
); );
} }

View File

@@ -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"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M12 5v14M5 12h14" strokeLinecap="round" /> <path d="M12 5v14M5 12h14" strokeLinecap="round" />
</svg> </svg>
·
</button> </button>
</div> </div>