Files
ai-toy-patent-workflow/src/app/page.tsx
2026-05-19 00:18:42 +08:00

243 lines
8.7 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 { 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>
);
}