243 lines
8.7 KiB
TypeScript
243 lines
8.7 KiB
TypeScript
'use client';
|
||
|
||
import { useCallback, useEffect, useState } from 'react';
|
||
import PromptPanel from '@/components/PromptPanel';
|
||
import ResultGrid from '@/components/ResultGrid';
|
||
import Sidebar from '@/components/Sidebar';
|
||
import PackPanel from '@/components/PackPanel';
|
||
import type {
|
||
GenImage,
|
||
GenSession,
|
||
GenerateAllPacksResponse,
|
||
GeneratePackResponse,
|
||
GenerateResponse,
|
||
LockCharacterResponse,
|
||
PackKind,
|
||
VideoGenerationResponse,
|
||
} from '@/lib/types';
|
||
|
||
export default function Home() {
|
||
const [sessions, setSessions] = useState<GenSession[]>([]);
|
||
const [current, setCurrent] = useState<GenSession | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [loadingKind, setLoadingKind] = useState<PackKind | null>(null);
|
||
const [allLoading, setAllLoading] = useState(false);
|
||
const [characterLoading, setCharacterLoading] = useState(false);
|
||
const [videoLoading, setVideoLoading] = useState(false);
|
||
const [provider, setProvider] = useState<string>('?');
|
||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||
|
||
const refreshSessions = useCallback(async () => {
|
||
const r = await fetch('/api/sessions');
|
||
const d = await r.json();
|
||
setSessions(d.sessions);
|
||
return d.sessions as GenSession[];
|
||
}, []);
|
||
|
||
useEffect(() => { refreshSessions(); }, [refreshSessions]);
|
||
|
||
async function handleGenerate(opts: { prompt: string; refImages: string[]; count: number; style?: string }) {
|
||
setLoading(true);
|
||
try {
|
||
const r = await fetch('/api/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(opts),
|
||
});
|
||
if (!r.ok) {
|
||
alert('生成失败:' + (await r.text()));
|
||
return;
|
||
}
|
||
const d: GenerateResponse = await r.json();
|
||
setProvider(d.provider);
|
||
const all = await refreshSessions();
|
||
const s = all.find(x => x.id === d.sessionId) ?? null;
|
||
setCurrent(s);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
async function handleAction(imageId: string, action: 'select' | 'reject' | 'reset') {
|
||
if (!current) return;
|
||
const r = await fetch('/api/select', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ sessionId: current.id, imageId, action }),
|
||
});
|
||
if (!r.ok) return;
|
||
const d: { image: GenImage } = await r.json();
|
||
setCurrent(prev => prev ? {
|
||
...prev,
|
||
images: prev.images.map(i => i.id === imageId ? d.image : i),
|
||
} : prev);
|
||
refreshSessions();
|
||
}
|
||
|
||
async function handleGeneratePack(image: GenImage, kind: PackKind) {
|
||
if (!current || loadingKind) return;
|
||
setLoadingKind(kind);
|
||
try {
|
||
const r = await fetch('/api/packs/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ sessionId: current.id, imageId: image.id, kind }),
|
||
});
|
||
if (!r.ok) {
|
||
alert('素材包生成失败:' + (await r.text()));
|
||
return;
|
||
}
|
||
const d: GeneratePackResponse = await r.json();
|
||
setProvider(d.provider);
|
||
const all = await refreshSessions();
|
||
const updated = all.find(x => x.id === current.id) ?? null;
|
||
setCurrent(updated);
|
||
} finally {
|
||
setLoadingKind(null);
|
||
}
|
||
}
|
||
|
||
async function reloadCurrent(sessionId: string) {
|
||
const all = await refreshSessions();
|
||
const updated = all.find(x => x.id === sessionId) ?? null;
|
||
setCurrent(updated);
|
||
return updated;
|
||
}
|
||
|
||
async function handleLockCharacter(image: GenImage) {
|
||
if (!current || characterLoading) return;
|
||
setCharacterLoading(true);
|
||
try {
|
||
const r = await fetch('/api/character/lock', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ sessionId: current.id, imageId: image.id, force: true }),
|
||
});
|
||
if (!r.ok) {
|
||
alert('角色锁定失败:' + (await r.text()));
|
||
return;
|
||
}
|
||
const d: LockCharacterResponse = await r.json();
|
||
setProvider(d.provider);
|
||
await reloadCurrent(current.id);
|
||
} finally {
|
||
setCharacterLoading(false);
|
||
}
|
||
}
|
||
|
||
async function handleGenerateAll(image: GenImage) {
|
||
if (!current || allLoading) return;
|
||
setAllLoading(true);
|
||
try {
|
||
const r = await fetch('/api/packs/generate-all', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ sessionId: current.id, imageId: image.id }),
|
||
});
|
||
if (!r.ok) {
|
||
alert('完整三包生成失败:' + (await r.text()));
|
||
return;
|
||
}
|
||
const d: GenerateAllPacksResponse = await r.json();
|
||
setProvider(d.provider);
|
||
await reloadCurrent(current.id);
|
||
} finally {
|
||
setAllLoading(false);
|
||
}
|
||
}
|
||
|
||
async function handleGenerateVideo(image: GenImage, promptTemplate: string) {
|
||
if (!current || videoLoading) return;
|
||
setVideoLoading(true);
|
||
try {
|
||
const character = current.characterSpec
|
||
? `${current.characterSpec.name},${current.characterSpec.oneLiner}`
|
||
: current.prompt;
|
||
const r = await fetch('/api/video/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
prompt: promptTemplate.replace('{character}', character),
|
||
imageUrl: image.url,
|
||
duration: 6,
|
||
ratio: '16:9',
|
||
resolution: '1080p',
|
||
}),
|
||
});
|
||
if (!r.ok) {
|
||
alert('Seedance 视频提交失败:' + (await r.text()));
|
||
return;
|
||
}
|
||
const d: VideoGenerationResponse = await r.json();
|
||
alert(`Seedance 任务已提交:${d.taskId ?? d.status}`);
|
||
} finally {
|
||
setVideoLoading(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="flex h-screen text-white">
|
||
<Sidebar
|
||
open={sidebarOpen}
|
||
onToggle={() => setSidebarOpen(v => !v)}
|
||
sessions={sessions}
|
||
currentId={current?.id ?? null}
|
||
onPick={id => setCurrent(sessions.find(s => s.id === id) ?? null)}
|
||
onNew={() => setCurrent(null)}
|
||
/>
|
||
<main className="flex-1 overflow-y-auto">
|
||
<div className="mx-auto max-w-[1200px] px-10 py-10">
|
||
<header className="flex items-start justify-between mb-10">
|
||
<div>
|
||
<span className="section-eyebrow">AI Toy Workflow</span>
|
||
<h1 className="mt-2 text-[30px] font-semibold tracking-tight leading-tight">
|
||
把一句想法
|
||
<span className="bg-gradient-to-r from-violet-300 via-fuchsia-300 to-blue-300 bg-clip-text text-transparent"> 变成专利、生产、宣发的素材包</span>
|
||
</h1>
|
||
<p className="text-sm text-white/50 mt-2 max-w-[560px] leading-relaxed">
|
||
批量出意向 · 九宫格快筛 · 锁定角色 · 一键三包 · Seedance 视频
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-2 pt-1 shrink-0">
|
||
<span className={provider === 'gpt' ? 'chip chip-live' : provider === '?' ? 'chip chip-neutral' : 'chip chip-mock'}>
|
||
<span className={`w-1.5 h-1.5 rounded-full ${provider === 'gpt' ? 'bg-emerald-400' : provider === '?' ? 'bg-white/40' : 'bg-amber-400'}`} />
|
||
{provider === 'gpt' ? 'GPT · 最高规格' : provider === 'mock' ? 'Mock · 占位图' : provider === '?' ? '待连接' : provider}
|
||
</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div className="space-y-8">
|
||
<PromptPanel onGenerate={handleGenerate} loading={loading} />
|
||
{current && (
|
||
<section className="space-y-5">
|
||
<div className="flex items-end justify-between">
|
||
<div>
|
||
<span className="section-eyebrow">Step · 02 · Quick Screen</span>
|
||
<h2 className="mt-2 text-lg font-semibold text-white">本次生成</h2>
|
||
<p className="text-xs text-white/40 mt-1">
|
||
{new Date(current.createdAt).toLocaleString('zh-CN')}
|
||
</p>
|
||
</div>
|
||
<code className="text-[11px] text-white/30 font-mono">{current.id}</code>
|
||
</div>
|
||
<ResultGrid images={current.images} onAction={handleAction} />
|
||
<PackPanel
|
||
session={current}
|
||
loadingKind={loadingKind}
|
||
allLoading={allLoading}
|
||
characterLoading={characterLoading}
|
||
videoLoading={videoLoading}
|
||
onGenerate={handleGeneratePack}
|
||
onGenerateAll={handleGenerateAll}
|
||
onLockCharacter={handleLockCharacter}
|
||
onGenerateVideo={handleGenerateVideo}
|
||
/>
|
||
</section>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|