feat(core/frontend): wire settings/avatar/image-gen + real data render (library/product-detail/pipeline)

App.tsx: thread saveProfile/changePassword/uploadAvatar/generateImages handlers + assets prop to pages.
- settings.tsx: profile save / password modal / avatar upload wired; notification/theme prefs -> localStorage
- library.tsx + product-detail: asset thumbnails + grids render real TOS preview_url
- ai-tools ImageWorkbenchPage: 生成图片 wired to /api/ai/generate-image, renders returned assets
- pipeline.tsx stage2-5: base_assets/storyboard/video_segments(adopted_asset)/timeline(clips/subtitles/bgm)
  rendered from real project data; graceful empty states
- types.ts: +VideoSegment.adopted_asset, +Timeline.subtitle_tracks/bgm_tracks
verified: tsc --noEmit clean; screenshots confirm pipeline stages 2-5 + product-detail render real data+images
(demo asset object_keys re-pointed to image objects so thumbnails resolve)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-05 16:20:10 +08:00
co-authored by Claude Opus 4.8
parent 603584b46b
commit 099bf0e6aa
11 changed files with 761 additions and 305 deletions
+42 -15
View File
@@ -271,7 +271,8 @@ export function ImageWorkbenchPage({
assets,
modelConfigs,
onBack,
navigate
navigate,
onGenerate
}: {
mode: WorkMode;
products: Product[];
@@ -279,6 +280,7 @@ export function ImageWorkbenchPage({
modelConfigs: ModelConfig[];
onBack: () => void;
navigate?: (page: Page) => void;
onGenerate: (payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number }) => Promise<{ assets: Asset[] } | null>;
}) {
const meta = MODE_META[mode];
const [productId, setProductId] = useState(products[0]?.id || "");
@@ -287,6 +289,8 @@ export function ImageWorkbenchPage({
const [ratio, setRatio] = useState(meta.ratio);
const [count, setCount] = useState("4");
const [pickedIds, setPickedIds] = useState<string[]>([]);
const [generating, setGenerating] = useState(false);
const [results, setResults] = useState<Asset[] | null>(null);
const imageModels = modelConfigs.filter((model) => model.capability.includes("image"));
const modelOptions = useMemo(() => modelConfigs.slice(0, 6), [modelConfigs]);
@@ -304,6 +308,19 @@ export function ImageWorkbenchPage({
const ratioVar = ratio.replace(":", " / ");
const candidateCount = Math.max(1, Number(count) || 4);
const canGenerate = prompt.trim().length > 0 && !generating;
async function runGenerate() {
if (!canGenerate) return;
setGenerating(true);
try {
const result = await onGenerate({ prompt: prompt.trim(), mode, count: candidateCount });
// 成功:渲染真图;失败(null):保留原占位,不改 results
if (result?.assets) setResults(result.assets);
} finally {
setGenerating(false);
}
}
return (
<div className="image-workbench">
@@ -326,9 +343,9 @@ export function ImageWorkbenchPage({
方案 A
</button>
)}
<button className="btn btn-primary" type="button">
<WandSparkles size={13} />
生成图片
<button className="btn btn-primary" type="button" onClick={runGenerate} disabled={!canGenerate}>
{generating ? <span className="spinner" aria-hidden /> : <WandSparkles size={13} />}
{generating ? "生成中…" : "生成图片"}
</button>
</div>
@@ -485,9 +502,9 @@ export function ImageWorkbenchPage({
</div>
<div className="iw-cta">
<button className="btn btn-primary" type="button">
<WandSparkles size={13} />
立即生成
<button className="btn btn-primary" type="button" onClick={runGenerate} disabled={!canGenerate}>
{generating ? <span className="spinner" aria-hidden /> : <WandSparkles size={13} />}
{generating ? "生成中…" : "立即生成"}
</button>
<div className="iw-cta-hint">
// {imageModels[0]?.display_name || "Volcano Image"} · 预估 {meta.title}
@@ -513,14 +530,24 @@ export function ImageWorkbenchPage({
<span className="m-sep">|</span>
<span>{product?.title || meta.title}</span>
</div>
<div className="gen-images" style={{ "--cols": candidateCount >= 4 ? 4 : 2, "--ratio": ratioVar } as React.CSSProperties}>
{Array.from({ length: candidateCount }).map((_, index) => (
<div className="gen-image" key={index}>
<div className="placeholder">
<span className="ph-frame">
{ratio} · #{index + 1}
</span>
</div>
<div
className="gen-images"
style={{ "--cols": (results?.length ?? candidateCount) >= 4 ? 4 : 2, "--ratio": ratioVar } as React.CSSProperties}
>
{(results && results.length > 0
? results.map((asset, index) => ({ key: asset.id, index, url: asset.files?.[0]?.preview_url }))
: Array.from({ length: candidateCount }).map((_, index) => ({ key: `ph-${index}`, index, url: undefined }))
).map(({ key, index, url }) => (
<div className="gen-image" key={key}>
{url ? (
<img className="gen-image-img" src={url} alt={`${meta.title} #${index + 1}`} loading="lazy" />
) : (
<div className="placeholder">
<span className="ph-frame">
{ratio} · #{index + 1}
</span>
</div>
)}
<div className="gen-image-actions">
<button className="gen-img-btn" type="button" title="重跑">
<RefreshCw size={14} />