暂存修改
This commit is contained in:
+613
File diff suppressed because one or more lines are too long
Vendored
+3
-1
@@ -1,3 +1,4 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
@@ -28,10 +29,11 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-M8UKiCMs.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BhfopyFy.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Generated
+82
@@ -17,6 +17,7 @@
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-quill-new": "^3.8.3",
|
||||
"react-router-dom": "^7.15.0",
|
||||
"three": "^0.184.0",
|
||||
"uuid": "^14.0.0",
|
||||
@@ -2826,6 +2827,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -2833,6 +2840,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
|
||||
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
@@ -3615,6 +3628,25 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -3822,6 +3854,12 @@
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parchment": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
|
||||
"integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -4138,6 +4176,35 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quill": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
|
||||
"integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"parchment": "^3.0.0",
|
||||
"quill-delta": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=8.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/quill-delta": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
|
||||
"integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-diff": "^1.3.0",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.isequal": "^4.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rangetouch": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rangetouch/-/rangetouch-2.0.1.tgz",
|
||||
@@ -4189,6 +4256,21 @@
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-quill-new": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/react-quill-new/-/react-quill-new-3.8.3.tgz",
|
||||
"integrity": "sha512-c96PYqFTo0pI4R3e79B3rH9LUIce1kIQbmTBu/imJQZk8305ogyLyBqKKjG2UoInDlquXqePSzmBo2aVia3ttw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash-es": "^4.17.21",
|
||||
"quill": "~2.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"quill-delta": "^5.1.0",
|
||||
"react": "^16 || ^17 || ^18 || ^19",
|
||||
"react-dom": "^16 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.0.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-quill-new": "^3.8.3",
|
||||
"react-router-dom": "^7.15.0",
|
||||
"three": "^0.184.0",
|
||||
"uuid": "^14.0.0",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { api, setToken, clearToken } from './client';
|
||||
import * as mock from './mock';
|
||||
import type {
|
||||
User, CreditRecord, Project, GenerationRecord, OptimizeParams, GenerateParams, OptimizeResult,
|
||||
User, CreditRecord, Project, GenerationRecord, OptimizeParams, OptimizeResult,
|
||||
Industry, IndustryConfig, AdminUser, AdminStats, ModelConfig, SystemConfig, AdminNotification,
|
||||
PrivatePortraitConfig, PrivatePortraitProjectListOut, PrivatePortraitProject, PrivatePortraitValidateSession,
|
||||
PrivatePortraitProjectCreateWithValidateOut, PrivatePortraitAssetListOut, PrivatePortraitAsset, PrivatePortraitSelectableAssetListOut,
|
||||
@@ -112,7 +112,11 @@ export async function optimizePrompt(
|
||||
project_id: projectId,//项目id
|
||||
gen_type:params.genType,//生成类型
|
||||
prompt: params.prompt,
|
||||
engine_id: params.engineId,
|
||||
include_media_references: params.includeMediaReferences ?? false,
|
||||
duration: params.duration,
|
||||
aspect_ratio: params.aspectRatio || null,
|
||||
resolution: params.resolution || null,
|
||||
references: params.references || null,
|
||||
idempotency_key: params.idempotencyKey || null,
|
||||
image_size: params.image_size || null,
|
||||
@@ -266,15 +270,14 @@ export async function deleteUpload(url: string): Promise<void> {
|
||||
export async function updateRecordPrompt(recordId: string, optimizedPrompt: string): Promise<void> {
|
||||
await api.put(`/generation-records/${recordId}/prompt`, { optimized_prompt: optimizedPrompt });
|
||||
}
|
||||
export async function generateVideo(recordId: string, params: GenerateParams): Promise<GenerationRecord> {
|
||||
export async function generateVideo(recordId: string): Promise<GenerationRecord> {
|
||||
if (USE_MOCK) return mock.mockGenerateVideo(recordId);
|
||||
return api.post<GenerationRecord>(`/generation-records/${recordId}/generate`, {
|
||||
engine_id: params.engineId,
|
||||
include_media_references: params.includeMediaReferences ?? false,
|
||||
aspect_ratio: params.aspectRatio,
|
||||
resolution: params.resolution,
|
||||
image_size: params.imageSize,
|
||||
});
|
||||
return api.post<GenerationRecord>(`/generation-records/${recordId}/generate`);
|
||||
}
|
||||
|
||||
export async function retryGeneration(recordId: string): Promise<GenerationRecord> {
|
||||
if (USE_MOCK) return mock.mockGenerateVideo(recordId);
|
||||
return api.post<GenerationRecord>(`/generation-records/${recordId}/retry`);
|
||||
}
|
||||
// ── Credits ───────────────────────────────────────────────
|
||||
export async function getCredits(page = 1, pageSize = 20): Promise<{ credits: number; records: CreditRecord[]; total: number }> {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type {
|
||||
CreditRecord,
|
||||
Project,
|
||||
GenerationRecord,
|
||||
GenerateParams,
|
||||
OptimizeParams,
|
||||
OptimizeResult,
|
||||
LoginParams,
|
||||
@@ -208,7 +207,7 @@ export async function mockOptimizePrompt(
|
||||
await delay(1500);
|
||||
|
||||
const project = MOCK_PROJECTS.find((p) => p.id === projectId);
|
||||
const cost = Math.round(80 + params.prompt.length * 0.5 + params.duration * 2);
|
||||
const cost = Math.round(80 + params.prompt.length * 0.5 + (params.duration || 0) * 2);
|
||||
|
||||
if (currentUser) {
|
||||
currentUser.credits -= cost;
|
||||
@@ -239,18 +238,28 @@ export async function mockOptimizePrompt(
|
||||
projectName: project?.name ?? '未知项目',
|
||||
originalPrompt: params.prompt,
|
||||
optimizedPrompt,
|
||||
duration: params.duration,
|
||||
aspectRatio: params.aspectRatio as any,
|
||||
resolution: params.resolution as any,
|
||||
duration: params.genType === 'video' ? params.duration : undefined,
|
||||
aspectRatio: params.genType === 'video' ? params.aspectRatio as any : undefined,
|
||||
resolution: params.genType === 'video' ? params.resolution as any : undefined,
|
||||
status: 'prompt_optimized',
|
||||
creditsCost: cost,
|
||||
textCreditsCost: cost,
|
||||
textTokensUsed: 0,
|
||||
videoTokensUsed: 0,
|
||||
imageSize: params.resolution || '1080p',
|
||||
imageProportion: params.aspectRatio || '16:9',
|
||||
imagePx: '1920x1080',
|
||||
imageSize: params.genType === 'image' ? params.image_size : undefined,
|
||||
imageProportion: params.genType === 'image' ? params.image_proportion : undefined,
|
||||
imagePx: params.genType === 'image' ? params.image_px : undefined,
|
||||
imageUrl: '',
|
||||
engineId: params.engineId,
|
||||
engineName: params.engineId,
|
||||
engineSnapshot: { id: params.engineId, name: params.engineId },
|
||||
includeMediaReferences: Boolean(params.includeMediaReferences),
|
||||
configComplete: true,
|
||||
canGenerate: true,
|
||||
canRetry: false,
|
||||
shouldPoll: false,
|
||||
clientStatus: 'ready',
|
||||
operationPhase: 'prompt',
|
||||
createdAt: new Date().toLocaleString('zh-CN'),
|
||||
};
|
||||
|
||||
@@ -265,7 +274,18 @@ export async function mockGenerateVideo(recordId: string): Promise<GenerationRec
|
||||
if (!record) throw new Error('记录不存在');
|
||||
|
||||
record.status = 'completed';
|
||||
record.videoUrl = `https://example.com/video-${recordId}.mp4`;
|
||||
record.canGenerate = false;
|
||||
record.canRetry = false;
|
||||
record.shouldPoll = false;
|
||||
record.clientStatus = 'success';
|
||||
record.operationPhase = 'resource';
|
||||
if (record.genType === 'image') {
|
||||
record.imageUrl = `https://example.com/image-${recordId}.png`;
|
||||
record.videoUrl = undefined;
|
||||
} else {
|
||||
record.videoUrl = `https://example.com/video-${recordId}.mp4`;
|
||||
record.imageUrl = undefined;
|
||||
}
|
||||
record.generatedAt = new Date().toLocaleString('zh-CN');
|
||||
|
||||
return record;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Tag, Button } from 'antd';
|
||||
import { BellOutlined, ThunderboltOutlined, GiftOutlined, StarOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { BellOutlined, ThunderboltOutlined, GiftOutlined, StarOutlined, CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { getNotifications, markNotificationRead } from '../api';
|
||||
import { useAuthStore } from '../store/useAuthStore';
|
||||
|
||||
@@ -105,94 +105,61 @@ const NotificationPopup: React.FC = () => {
|
||||
background: 'rgba(0,0,0,0.45)', backdropFilter: 'blur(6px)',
|
||||
}} onClick={handleClose}>
|
||||
<div onClick={e => e.stopPropagation()} style={{
|
||||
width: 420,
|
||||
borderRadius: 24,
|
||||
width: 520,
|
||||
borderRadius: 24,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 25px 80px rgba(0,0,0,0.15)',
|
||||
boxShadow: '0 30px 100px rgba(0,0,0,0.2)',
|
||||
animation: 'notificationPop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
background: '#ffffff',
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
background: tc.gradient,
|
||||
padding: '24px 28px 20px',
|
||||
position: 'relative',
|
||||
padding: '16px 28px 14px',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
right: -50,
|
||||
top: -50,
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: '50%',
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
right: -50,
|
||||
top: -50,
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 60%)',
|
||||
filter: 'blur(30px)',
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
right: 30,
|
||||
top: 10,
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.06)',
|
||||
filter: 'blur(15px)',
|
||||
}} />
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', position: 'relative' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 14,
|
||||
background: 'rgba(255,255,255,0.15)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 20,
|
||||
color: '#fff',
|
||||
}}>{tc.icon}</div>
|
||||
<div>
|
||||
<div style={{
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
fontSize: 12,
|
||||
marginBottom: 4,
|
||||
fontWeight: 500,
|
||||
}}>新消息通知</div>
|
||||
<div style={{
|
||||
color: '#fff',
|
||||
fontSize: 28,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-1px',
|
||||
}}>{current.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
{/* Top row: type badge + close */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||||
<Tag style={{ background: 'rgba(255,255,255,0.2)', border: 'none', color: '#fff', borderRadius: 6, padding: '2px 10px', fontSize: 11, fontWeight: 500 }}>
|
||||
{tc.icon} {tc.label}
|
||||
</Tag>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s ease',
|
||||
outline: 'none',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.background = 'rgba(255,255,255,0.2)';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.background = 'rgba(255,255,255,0.1)';
|
||||
width: 28, height: 28, borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
border: 'none', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
transition: 'all 0.2s ease', outline: 'none',
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.2)'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.1)'; }}
|
||||
>
|
||||
<CloseOutlined style={{ color: '#fff', fontSize: 14 }} />
|
||||
<CloseOutlined style={{ color: '#fff', fontSize: 12 }} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Title + time row */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12 }}>
|
||||
<div style={{ color: '#fff', fontSize: 22, fontWeight: 700, letterSpacing: '-0.5px' }}>
|
||||
{current.title}
|
||||
</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.6)', fontSize: 11, whiteSpace: 'nowrap', flexShrink: 0 }}>
|
||||
{current.createdAt ? new Date(current.createdAt).toLocaleString('zh-CN') : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
@@ -201,48 +168,67 @@ const NotificationPopup: React.FC = () => {
|
||||
padding: '24px 28px 28px',
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '18px 20px',
|
||||
background: '#f8fafc',
|
||||
padding: '18px 20px',
|
||||
background: '#f8fafc',
|
||||
borderRadius: 16,
|
||||
marginBottom: 20,
|
||||
marginBottom: 20,
|
||||
border: '1px solid #f1f5f9',
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: 14,
|
||||
color: '#475569',
|
||||
<div style={{
|
||||
fontSize: 14,
|
||||
color: '#475569',
|
||||
lineHeight: 1.7,
|
||||
}}>
|
||||
{current.content}
|
||||
</div>
|
||||
}} dangerouslySetInnerHTML={{ __html: current.content }} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{/* Left: navigation with left/right buttons */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Tag
|
||||
color={tc.gradient.includes('#6366f1') ? '#6366f1' : tc.gradient.includes('#f59e0b') ? '#f59e0b' : '#8b5cf6'}
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
border: 'none',
|
||||
padding: '3px 10px',
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
background: '#f1f5f9',
|
||||
color: '#64748b',
|
||||
}}
|
||||
>
|
||||
{tc.label}
|
||||
</Tag>
|
||||
{notifications.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setCurrentIndex(currentIndex > 0 ? currentIndex - 1 : notifications.length - 1); }}
|
||||
style={{
|
||||
width: 30, height: 30, borderRadius: '50%',
|
||||
background: '#f1f5f9', border: 'none', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
transition: 'all 0.2s', outline: 'none',
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = '#e2e8f0'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = '#f1f5f9'; }}
|
||||
>
|
||||
<LeftOutlined style={{ color: '#64748b', fontSize: 12 }} />
|
||||
</button>
|
||||
<span style={{ fontSize: 12, color: '#64748b', fontWeight: 500, minWidth: 36, textAlign: 'center' }}>
|
||||
{currentIndex + 1}/{notifications.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setCurrentIndex(currentIndex < notifications.length - 1 ? currentIndex + 1 : 0); }}
|
||||
style={{
|
||||
width: 30, height: 30, borderRadius: '50%',
|
||||
background: '#f1f5f9', border: 'none', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
transition: 'all 0.2s', outline: 'none',
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = '#e2e8f0'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = '#f1f5f9'; }}
|
||||
>
|
||||
<RightOutlined style={{ color: '#64748b', fontSize: 12 }} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleAcknowledge}
|
||||
{/* Right: acknowledge button */}
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleAcknowledge}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
fontWeight: 600,
|
||||
borderRadius: 12,
|
||||
fontWeight: 600,
|
||||
height: 40,
|
||||
background: tc.gradient,
|
||||
background: tc.gradient,
|
||||
border: 'none',
|
||||
paddingLeft: 28,
|
||||
paddingLeft: 28,
|
||||
paddingRight: 28,
|
||||
boxShadow: `0 6px 16px ${tc.glowColor}`,
|
||||
transition: 'all 0.2s ease',
|
||||
@@ -261,28 +247,6 @@ const NotificationPopup: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{notifications.length > 1 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 6, marginTop: 20 }}>
|
||||
{notifications.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrentIndex(i)}
|
||||
style={{
|
||||
width: i === currentIndex ? 20 : 5,
|
||||
height: 5,
|
||||
borderRadius: 3,
|
||||
background: i === currentIndex
|
||||
? (tc.gradient.includes('#6366f1') ? '#6366f1' : tc.gradient.includes('#f59e0b') ? '#f59e0b' : '#8b5cf6')
|
||||
: '#e2e8f0',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<style>{`
|
||||
|
||||
@@ -502,6 +502,7 @@ const GeneratePage: React.FC = () => {
|
||||
fetchRecords,
|
||||
optimizePrompt,
|
||||
generateVideo,
|
||||
retryGeneration,
|
||||
} = useAppStore();
|
||||
const recordItems = records.items;
|
||||
const { user } = useAuthStore();
|
||||
@@ -635,11 +636,6 @@ const GeneratePage: React.FC = () => {
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [expandedGroup, setExpandedGroup] = useState<string | null>(null);
|
||||
// Per-record param selections for history prompt_optimized records
|
||||
const [historyParams, setHistoryParams] = useState<
|
||||
Record<string, { aspectRatio?: AspectRatio; resolution?: Resolution; engineId?: string; includeMediaReferences?: boolean }>
|
||||
>({});
|
||||
|
||||
// Video preview modal
|
||||
const [previewVideoUrl, setPreviewVideoUrl] = useState<string | null>(null);
|
||||
|
||||
@@ -1301,7 +1297,11 @@ const GeneratePage: React.FC = () => {
|
||||
const pending = localStorage.getItem("pending_optimize");
|
||||
if (!pending) return;
|
||||
try {
|
||||
const { key, projectId: pId, prompt, duration, ts } = JSON.parse(pending);
|
||||
const {
|
||||
key, projectId: pId, prompt, duration, engineId, genType, aspectRatio,
|
||||
resolution, imageSize, imageProportion, imagePx, includeMediaReferences: pendingIncludeReferences,
|
||||
referenceIds, ts,
|
||||
} = JSON.parse(pending);
|
||||
if (Date.now() - ts > 5 * 60 * 1000 || pId !== projectId) {
|
||||
localStorage.removeItem("pending_optimize");
|
||||
return;
|
||||
@@ -1314,6 +1314,15 @@ const GeneratePage: React.FC = () => {
|
||||
r.projectId === pId &&
|
||||
r.originalPrompt === prompt &&
|
||||
r.duration === duration &&
|
||||
r.engineId === engineId &&
|
||||
String(r.genType || "video") === String(genType || "video") &&
|
||||
String(r.aspectRatio || "") === String(aspectRatio || "") &&
|
||||
String(r.resolution || "") === String(resolution || "") &&
|
||||
String(r.imageSize || "") === String(imageSize || "") &&
|
||||
String(r.imageProportion || "") === String(imageProportion || "") &&
|
||||
String(r.imagePx || "") === String(imagePx || "") &&
|
||||
Boolean(r.includeMediaReferences) === Boolean(pendingIncludeReferences) &&
|
||||
JSON.stringify((r.references || []).map((item) => item.upload_resource_id || item.url).filter(Boolean).sort()) === JSON.stringify((referenceIds || []).slice().sort()) &&
|
||||
r.status === "prompt_optimized" &&
|
||||
new Date(r.createdAt).getTime() > ts - 10000,
|
||||
);
|
||||
@@ -1370,8 +1379,19 @@ const GeneratePage: React.FC = () => {
|
||||
|
||||
// Media credits estimate for step 2 (video or image)
|
||||
const estimatedVideoCredits = mediaType === "image"
|
||||
? getImageCreditsFromCimage(selectedResolution)
|
||||
: calcVideoCredits(videoDuration, videoResolution);
|
||||
? getImageCreditsFromCimage(
|
||||
currentRecord?.imageSize || selectedResolution,
|
||||
currentRecord?.engineId || selectedEngineId,
|
||||
currentRecord?.includeMediaReferences ?? includeMediaReferences,
|
||||
currentRecord?.references || references,
|
||||
)
|
||||
: calcVideoCredits(
|
||||
currentRecord?.duration || videoDuration,
|
||||
(currentRecord?.resolution || videoResolution) as Resolution,
|
||||
currentRecord?.engineId || selectedEngineId,
|
||||
currentRecord?.includeMediaReferences ?? includeMediaReferences,
|
||||
currentRecord?.references || references,
|
||||
);
|
||||
const canAffordVideo = userCredits >= estimatedVideoCredits;
|
||||
|
||||
// Step 1: Optimize prompt (text credits)
|
||||
@@ -1386,6 +1406,14 @@ const GeneratePage: React.FC = () => {
|
||||
message.error("请输入视频/图片描述");
|
||||
return;
|
||||
}
|
||||
if (!selectedEngineId) {
|
||||
message.error("请选择生成引擎");
|
||||
return;
|
||||
}
|
||||
if (mediaType === "video" && (!videoAspectRatio || !videoResolution)) {
|
||||
message.error("请选择视频比例和分辨率");
|
||||
return;
|
||||
}
|
||||
|
||||
setOptimizing(true);
|
||||
const optionEntries = Object.entries(selectedOptions).map(
|
||||
@@ -1404,20 +1432,32 @@ const GeneratePage: React.FC = () => {
|
||||
projectId,
|
||||
prompt: fullPrompt,
|
||||
duration: videoDuration,
|
||||
engineId: selectedEngineId,
|
||||
genType: mediaType,
|
||||
aspectRatio: mediaType === "video" ? videoAspectRatio : "",
|
||||
resolution: mediaType === "video" ? videoResolution : "",
|
||||
imageSize: mediaType === "image" ? selectedResolution : "",
|
||||
imageProportion: mediaType === "image" ? selectedRatio : "",
|
||||
imagePx: mediaType === "image" ? `${width}x${height}` : "",
|
||||
includeMediaReferences,
|
||||
referenceIds: references.map((item) => item.upload_resource_id || item.url).filter(Boolean).sort(),
|
||||
ts: Date.now(),
|
||||
}),
|
||||
);
|
||||
inFlightOptimizeKey.current = idempotencyKey;
|
||||
const result = await optimizePrompt(projectId, {
|
||||
prompt: fullPrompt,
|
||||
duration: videoDuration,
|
||||
duration: mediaType === "video" ? videoDuration : undefined,
|
||||
genType: mediaType,
|
||||
resolution: selectedResolution,
|
||||
engineId: selectedEngineId,
|
||||
includeMediaReferences,
|
||||
aspectRatio: mediaType === "video" ? videoAspectRatio : undefined,
|
||||
resolution: mediaType === "video" ? videoResolution : undefined,
|
||||
references: references.length > 0 ? references : undefined,
|
||||
idempotencyKey,
|
||||
image_size: selectedResolution,
|
||||
image_proportion: selectedRatio,
|
||||
image_px: width + "x" + height,
|
||||
image_size: mediaType === "image" ? selectedResolution : undefined,
|
||||
image_proportion: mediaType === "image" ? selectedRatio : undefined,
|
||||
image_px: mediaType === "image" ? width + "x" + height : undefined,
|
||||
});
|
||||
// console.log("按钮触发", result);
|
||||
|
||||
@@ -1542,7 +1582,7 @@ const GeneratePage: React.FC = () => {
|
||||
const frontendState = recordStates[item.id];
|
||||
// 如果后端状态不是generating,说明任务已经完成(成功或失败),需要处理
|
||||
// 不管前端当前是什么状态,都要处理完成的任务
|
||||
if (latest.status !== "generating") {
|
||||
if (!(latest.shouldPoll ?? latest.status === "generating")) {
|
||||
completedIds.push(item.id);
|
||||
} else if (frontendState !== "generating") {
|
||||
// 如果后端状态是generating,但前端不是,更新为generating(处理页面刷新后状态丢失的情况)
|
||||
@@ -1617,7 +1657,7 @@ const GeneratePage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const generatingRecords = recordItems.filter((r) => r.status === "generating");
|
||||
const generatingRecords = recordItems.filter((r) => r.shouldPoll ?? r.status === "generating");
|
||||
generatingRecords.forEach((record) => {
|
||||
setRecordStates((p) => ({ ...p, [record.id]: "generating" }));
|
||||
startPolling(record.id);
|
||||
@@ -1625,10 +1665,6 @@ const GeneratePage: React.FC = () => {
|
||||
}, [recordItems]);
|
||||
|
||||
const handleGenerate = async (recordId: string) => {
|
||||
if (!canAffordVideo) {
|
||||
message.error("积分不足,请先充值");
|
||||
return;
|
||||
}
|
||||
setGenerating((p) => ({ ...p, [recordId]: true }));
|
||||
setRecordStates((p) => ({ ...p, [recordId]: "generating" }));
|
||||
message.loading({
|
||||
@@ -1637,13 +1673,15 @@ const GeneratePage: React.FC = () => {
|
||||
key: recordId,
|
||||
});
|
||||
try {
|
||||
const result = await generateVideo(recordId, {
|
||||
engineId: selectedEngineId || undefined,
|
||||
includeMediaReferences,
|
||||
aspectRatio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
imageSize: currentRecord?.imageSize || selectedResolution,
|
||||
});
|
||||
const record = projectRecords.find((item) => item.id === recordId) || currentRecord;
|
||||
if (!record || record.canGenerate === false) {
|
||||
throw new Error(record?.configFallbackHint || "该记录当前不可生成,请重新生成提词");
|
||||
}
|
||||
const nextPrompt = record.id === currentRecord?.id ? editedPrompt : editablePrompts[recordId];
|
||||
if (nextPrompt && nextPrompt !== record.optimizedPrompt) {
|
||||
await updateRecordPrompt(recordId, nextPrompt);
|
||||
}
|
||||
const result = await generateVideo(recordId);
|
||||
if (result.status === "failed") {
|
||||
setRecordStates((p) => ({ ...p, [recordId]: "failed" }));
|
||||
message.error({
|
||||
@@ -1698,18 +1736,10 @@ const GeneratePage: React.FC = () => {
|
||||
key: recordId,
|
||||
});
|
||||
try {
|
||||
// Save edited prompt first if changed
|
||||
const editedPrompt = editablePrompts[recordId];
|
||||
if (editedPrompt && editedPrompt !== record.optimizedPrompt) {
|
||||
await updateRecordPrompt(recordId, editedPrompt);
|
||||
if (!record.configComplete || record.canRetry === false) {
|
||||
throw new Error("该失败记录不能直接重试,请重新生成提词");
|
||||
}
|
||||
const result = await generateVideo(recordId, {
|
||||
engineId: record.engineId || selectedEngineId || undefined,
|
||||
includeMediaReferences: Boolean(record.includeMediaReferences),
|
||||
aspectRatio: record.aspectRatio || "16:9",
|
||||
resolution: record.resolution || "720p",
|
||||
imageSize: record.imageSize,
|
||||
});
|
||||
const result = await retryGeneration(recordId);
|
||||
|
||||
if (result.status === "failed") {
|
||||
setRecordStates((p) => ({ ...p, [recordId]: "failed" }));
|
||||
@@ -1758,6 +1788,10 @@ const GeneratePage: React.FC = () => {
|
||||
return resolveGenerationUiState({
|
||||
status: localStatus || record.status,
|
||||
pipelineStage: localState ? null : record.pipelineStage,
|
||||
clientStatus: localState ? null : record.clientStatus,
|
||||
shouldPoll: localState ? localState === "generating" : record.shouldPoll,
|
||||
canGenerate: record.canGenerate,
|
||||
canRetry: record.canRetry,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2792,6 +2826,36 @@ const GeneratePage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
flexWrap: "wrap",
|
||||
marginRight: 16,
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
value={selectedEngineId || undefined}
|
||||
onChange={handleGenerationEngineChange}
|
||||
placeholder="选择生成引擎"
|
||||
style={{ minWidth: 180 }}
|
||||
options={(mediaType === "image" ? imageEngines : videoEngines).map((item: any) => ({ value: item.id, label: item.name }))}
|
||||
/>
|
||||
{mediaType === "video" && (
|
||||
<>
|
||||
<Select value={videoAspectRatio} onChange={(value) => setVideoAspectRatio(value as AspectRatio)} style={{ width: 110 }} options={engineOptions.ratios.map((value) => ({ value, label: value }))} />
|
||||
<Select value={videoResolution} onChange={(value) => setVideoResolution(value as Resolution)} style={{ width: 110 }} options={engineOptions.resolutions.map((value) => ({ value, label: value }))} />
|
||||
</>
|
||||
)}
|
||||
{references.length > 0 && (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<Switch size="small" checked={includeMediaReferences} onChange={setIncludeMediaReferences} />
|
||||
<Typography.Text style={{ fontSize: 12 }}>生成时携带附件</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -3273,57 +3337,6 @@ const GeneratePage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Video params selection */}
|
||||
{/* {mediaType !== 'image' && (
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Typography.Text style={{ fontSize: 13, color: '#1a1a2e', fontWeight: 600, display: 'block', marginBottom: 12 }}>选择视频参数</Typography.Text>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '5px 12px', borderRadius: 8, background: '#f8f9fc', border: '1px solid #e2e8f0' }}>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}>时长</Typography.Text>
|
||||
<Typography.Text strong style={{ fontSize: 13, color: '#6366f1' }}>{videoDuration}s</Typography.Text>
|
||||
</div>
|
||||
<PortalDropdown label="比例" value={videoAspectRatio}
|
||||
options={engineOptions.ratios}
|
||||
expanded={expandedEngine === 'ratio'}
|
||||
onToggle={() => setExpandedEngine(expandedEngine === 'ratio' ? null : 'ratio')}
|
||||
onSelect={(v) => setVideoAspectRatio(v as AspectRatio)}
|
||||
onClose={() => setExpandedEngine(null)}
|
||||
/>
|
||||
<PortalDropdown label="分辨率" value={videoResolution}
|
||||
options={engineOptions.resolutions}
|
||||
expanded={expandedEngine === 'resolution'}
|
||||
onToggle={() => setExpandedEngine(expandedEngine === 'resolution' ? null : 'resolution')}
|
||||
onSelect={(v) => setVideoResolution(v as Resolution)}
|
||||
onClose={() => setExpandedEngine(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* Image params selection
|
||||
{mediaType === 'image' && currentRecord && (
|
||||
<div style={{ marginTop: 24, padding: 16, borderRadius: 12, background: '#f8f9fc' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-around' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Typography.Text style={{ fontSize: 11, color: '#94a3b8', display: 'block', marginBottom: 4 }}>分辨率</Typography.Text>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#1a1a2e' }}>{currentRecord.imageSize || '-'}</Typography.Text>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Typography.Text style={{ fontSize: 11, color: '#94a3b8', display: 'block', marginBottom: 4 }}>画面比例</Typography.Text>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#1a1a2e' }}>{currentRecord.imageProportion || '-'}</Typography.Text>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Typography.Text style={{ fontSize: 11, color: '#94a3b8', display: 'block', marginBottom: 4 }}>画面尺寸</Typography.Text>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#1a1a2e' }}>{currentRecord.imagePx || '-'}</Typography.Text>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Typography.Text style={{ fontSize: 11, color: '#94a3b8', display: 'block', marginBottom: 4 }}>消耗积分</Typography.Text>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#1a1a2e' }}>-</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* Video credits + generate button */}
|
||||
<div
|
||||
style={{
|
||||
@@ -3340,17 +3353,27 @@ const GeneratePage: React.FC = () => {
|
||||
<div style={{ minWidth: 220, marginRight: 16 }}>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>生成引擎</Typography.Text>
|
||||
<Select
|
||||
value={selectedEngineId || undefined}
|
||||
onChange={handleGenerationEngineChange}
|
||||
value={currentRecord.engineId || undefined}
|
||||
disabled
|
||||
style={{ width: '100%' }}
|
||||
options={(mediaType === 'image' ? imageEngines : videoEngines).map((item: any) => ({ value: item.id, label: item.name }))}
|
||||
/>
|
||||
{currentRecord?.references?.length ? (
|
||||
<div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch size="small" checked={includeMediaReferences} onChange={setIncludeMediaReferences} />
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b' }}>携带参考附件生成</Typography.Text>
|
||||
<Switch size="small" checked={Boolean(currentRecord.includeMediaReferences)} disabled />
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b' }}>提词阶段已冻结附件配置</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
{currentRecord.configComplete === false && (
|
||||
<Typography.Text
|
||||
type={currentRecord.canGenerate === false ? "danger" : "warning"}
|
||||
style={{ fontSize: 12, display: "block", marginTop: 8 }}
|
||||
>
|
||||
{currentRecord.configFallbackHint || (currentRecord.canGenerate === false
|
||||
? "旧版本配置不完整,请重新生成提词"
|
||||
: "旧版本配置缺失,提交生成时将由后端自动补齐")}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Video params selection */}
|
||||
@@ -3372,7 +3395,7 @@ const GeneratePage: React.FC = () => {
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
选择视频参数
|
||||
已冻结视频参数
|
||||
</Typography.Text>
|
||||
<div
|
||||
style={{
|
||||
@@ -3400,33 +3423,25 @@ const GeneratePage: React.FC = () => {
|
||||
strong
|
||||
style={{ fontSize: 13, color: "#6366f1" }}
|
||||
>
|
||||
{videoDuration}s
|
||||
{currentRecord.duration || videoDuration}s
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<PortalDropdown
|
||||
label="比例"
|
||||
value={videoAspectRatio}
|
||||
options={engineOptions.ratios}
|
||||
expanded={expandedEngine === "ratio"}
|
||||
onToggle={() =>
|
||||
setExpandedEngine(
|
||||
expandedEngine === "ratio" ? null : "ratio",
|
||||
)
|
||||
}
|
||||
onSelect={(v) => setVideoAspectRatio(v as AspectRatio)}
|
||||
value={(currentRecord.aspectRatio || videoAspectRatio) as string}
|
||||
options={[String(currentRecord.aspectRatio || videoAspectRatio)]}
|
||||
expanded={false}
|
||||
onToggle={() => undefined}
|
||||
onSelect={() => undefined}
|
||||
onClose={() => setExpandedEngine(null)}
|
||||
/>
|
||||
<PortalDropdown
|
||||
label="分辨率"
|
||||
value={videoResolution}
|
||||
options={engineOptions.resolutions}
|
||||
expanded={expandedEngine === "resolution"}
|
||||
onToggle={() =>
|
||||
setExpandedEngine(
|
||||
expandedEngine === "resolution" ? null : "resolution",
|
||||
)
|
||||
}
|
||||
onSelect={(v) => setVideoResolution(v as Resolution)}
|
||||
value={(currentRecord.resolution || videoResolution) as string}
|
||||
options={[String(currentRecord.resolution || videoResolution)]}
|
||||
expanded={false}
|
||||
onToggle={() => undefined}
|
||||
onSelect={() => undefined}
|
||||
onClose={() => setExpandedEngine(null)}
|
||||
/>
|
||||
</div>
|
||||
@@ -3533,9 +3548,7 @@ const GeneratePage: React.FC = () => {
|
||||
strong
|
||||
style={{ fontSize: 14, color: "#10b981" }}
|
||||
>
|
||||
{mediaType === "image" && currentRecord.imageSize
|
||||
? getImageCreditsFromCimage(currentRecord.imageSize) || estimatedVideoCredits
|
||||
: calcVideoCredits(videoDuration, videoResolution)}
|
||||
{estimatedVideoCredits}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text style={{ color: "#cbd5e1", fontSize: 18 }}>
|
||||
@@ -3551,7 +3564,7 @@ const GeneratePage: React.FC = () => {
|
||||
strong
|
||||
style={{ fontSize: 16, color: "#1a1a2e" }}
|
||||
>
|
||||
{(lastTextCredits + (mediaType === "image" ? getImageCreditsFromCimage(currentRecord.imageSize) : calcVideoCredits(videoDuration, videoResolution))).toFixed(2)}
|
||||
{(lastTextCredits + estimatedVideoCredits).toFixed(2)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3587,6 +3600,7 @@ const GeneratePage: React.FC = () => {
|
||||
loading={recordStates[currentRecord.id] === "generating"}
|
||||
disabled={
|
||||
!canAffordVideo ||
|
||||
currentRecord.canGenerate === false ||
|
||||
recordStates[currentRecord.id] === "generating" ||
|
||||
recordStates[currentRecord.id] === "done"
|
||||
}
|
||||
@@ -3611,7 +3625,20 @@ const GeneratePage: React.FC = () => {
|
||||
? "生成完成"
|
||||
: recordStates[currentRecord.id] === "generating"
|
||||
? "生成中..."
|
||||
: `生成${mediaType === "image" ? "图片" : "视频"} (${ (mediaType === "image" ? getImageCreditsFromCimage(currentRecord.imageSize) : calcVideoCredits(videoDuration, videoResolution))}积分)`}
|
||||
: `生成${mediaType === "image" ? "图片" : "视频"} (${(mediaType === "image"
|
||||
? getImageCreditsFromCimage(
|
||||
currentRecord.imageSize || selectedResolution,
|
||||
currentRecord.engineId,
|
||||
currentRecord.includeMediaReferences,
|
||||
currentRecord.references,
|
||||
)
|
||||
: calcVideoCredits(
|
||||
currentRecord.duration || videoDuration,
|
||||
(currentRecord.resolution || videoResolution) as Resolution,
|
||||
currentRecord.engineId,
|
||||
currentRecord.includeMediaReferences,
|
||||
currentRecord.references,
|
||||
))}积分)`}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -3810,7 +3837,7 @@ const GeneratePage: React.FC = () => {
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
待配置
|
||||
待生成
|
||||
</Tag>
|
||||
)}
|
||||
|
||||
@@ -3824,7 +3851,7 @@ const GeneratePage: React.FC = () => {
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{record.duration ? `${record.imageSize}` : "-"} ·{" "}
|
||||
{record.imageSize || "-"} ·{" "}
|
||||
{record.imageProportion || "-"} ·{" "}
|
||||
{record.imagePx || "-"} ·{" "}
|
||||
<span className="date-display" translate="no">
|
||||
@@ -4429,279 +4456,54 @@ const GeneratePage: React.FC = () => {
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* 将参数选择和视频生成按钮放到视频视频框 */}
|
||||
{/* 待生成记录只能读取提词阶段冻结的配置 */}
|
||||
{status === "prompt_optimized" && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "transparent",
|
||||
inset: 0,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
background: "#f8f9fc",
|
||||
border: "1px solid #e2e8f0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
|
||||
<Select
|
||||
size="small"
|
||||
value={historyParams[record.id]?.engineId || record.engineId || (type === 'image' ? imageEngines[0]?.id : videoEngines[0]?.id)}
|
||||
onChange={(value) => {
|
||||
const options = type === 'video' ? getVideoEngineOptions(value) : null;
|
||||
setHistoryParams((prev) => ({
|
||||
...prev,
|
||||
[record.id]: {
|
||||
...prev[record.id],
|
||||
engineId: value,
|
||||
aspectRatio: type === 'video' ? options!.ratios[0] as AspectRatio : prev[record.id]?.aspectRatio,
|
||||
resolution: type === 'video' ? options!.resolutions[0] as Resolution : prev[record.id]?.resolution,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
options={(type === 'image' ? imageEngines : videoEngines).map((item: any) => ({ value: item.id, label: item.name }))}
|
||||
style={{ minWidth: 160 }}
|
||||
/>
|
||||
{record.references?.length ? (
|
||||
<><Switch size="small" checked={Boolean(historyParams[record.id]?.includeMediaReferences ?? record.includeMediaReferences)} onChange={(checked) => setHistoryParams((prev) => ({ ...prev, [record.id]: { ...prev[record.id], includeMediaReferences: checked, aspectRatio: prev[record.id]?.aspectRatio || '' as AspectRatio, resolution: prev[record.id]?.resolution || '' as Resolution } }))} /><Typography.Text style={{ fontSize: 12 }}>携带附件</Typography.Text></>
|
||||
) : null}
|
||||
<div>
|
||||
<Typography.Text strong style={{ display: "block", marginBottom: 10 }}>冻结生成配置</Typography.Text>
|
||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>引擎:{record.engineName || record.engineId || "配置缺失"}</Typography.Text>
|
||||
{type === "video" ? (
|
||||
<>
|
||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>时长:{record.duration || "-"}秒</Typography.Text>
|
||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>比例:{record.aspectRatio || "-"}</Typography.Text>
|
||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>分辨率:{record.resolution || "-"}</Typography.Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>分辨率:{record.imageSize || "-"}</Typography.Text>
|
||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>比例:{record.imageProportion || "-"}</Typography.Text>
|
||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>尺寸:{record.imagePx || "-"}</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>附件:{record.includeMediaReferences ? "生成时携带" : "仅用于提词或不携带"}</Typography.Text>
|
||||
{record.configComplete === false && (
|
||||
<Typography.Text type={record.canGenerate === false ? "danger" : "warning"} style={{ display: "block", marginTop: 8 }}>
|
||||
{record.configFallbackHint || (record.canGenerate === false ? "旧版本配置不完整,请重新生成提词" : "旧版本配置缺失,提交生成时将由后端自动补齐")}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
{type === "video" && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
flexWrap: "wrap",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
translate="no"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
padding: "5px 12px",
|
||||
borderRadius: 8,
|
||||
background: "#f8f9fc",
|
||||
border: "1px solid #e2e8f0",
|
||||
marginLeft: 12,
|
||||
}}
|
||||
>
|
||||
<Typography.Text
|
||||
style={{ fontSize: 12, color: "#94a3b8" }}
|
||||
>
|
||||
时长
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
strong
|
||||
style={{ fontSize: 13, color: "#6366f1" }}
|
||||
>
|
||||
{record.duration}秒
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<PortalDropdown
|
||||
label="比例"
|
||||
value={
|
||||
historyParams[record.id]?.aspectRatio ||
|
||||
"选择比例"
|
||||
}
|
||||
options={getVideoEngineOptions(historyParams[record.id]?.engineId || record.engineId || videoEngines[0]?.id).ratios}
|
||||
expanded={
|
||||
expandedEngine === `ratio-${record.id}`
|
||||
}
|
||||
onToggle={() =>
|
||||
setExpandedEngine(
|
||||
expandedEngine === `ratio-${record.id}`
|
||||
? null
|
||||
: `ratio-${record.id}`,
|
||||
)
|
||||
}
|
||||
onSelect={(v) =>
|
||||
setHistoryParams((prev) => ({
|
||||
...prev,
|
||||
[record.id]: {
|
||||
...prev[record.id],
|
||||
aspectRatio: v as AspectRatio,
|
||||
resolution:
|
||||
prev[record.id]?.resolution || "",
|
||||
},
|
||||
}))
|
||||
}
|
||||
onClose={() => setExpandedEngine(null)}
|
||||
/>
|
||||
<PortalDropdown
|
||||
label="分辨率"
|
||||
value={
|
||||
historyParams[record.id]?.resolution ||
|
||||
"选择分辨率"
|
||||
}
|
||||
options={getVideoEngineOptions(historyParams[record.id]?.engineId || record.engineId || videoEngines[0]?.id).resolutions}
|
||||
expanded={
|
||||
expandedEngine === `res-${record.id}`
|
||||
}
|
||||
onToggle={() =>
|
||||
setExpandedEngine(
|
||||
expandedEngine === `res-${record.id}`
|
||||
? null
|
||||
: `res-${record.id}`,
|
||||
)
|
||||
}
|
||||
onSelect={(v) =>
|
||||
setHistoryParams((prev) => ({
|
||||
...prev,
|
||||
[record.id]: {
|
||||
...prev[record.id],
|
||||
aspectRatio:
|
||||
prev[record.id]?.aspectRatio || "",
|
||||
resolution: v as Resolution,
|
||||
},
|
||||
}))
|
||||
}
|
||||
onClose={() => setExpandedEngine(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<RocketOutlined />}
|
||||
block
|
||||
loading={generating[record.id]}
|
||||
disabled={
|
||||
type === "video" &&
|
||||
(!historyParams[record.id]?.aspectRatio ||
|
||||
!historyParams[record.id]?.resolution)
|
||||
}
|
||||
onClick={async () => {
|
||||
const params = historyParams[record.id];
|
||||
// 视频类型需要检查比例和分辨率
|
||||
if (type === "video") {
|
||||
if (
|
||||
!params?.aspectRatio ||
|
||||
!params?.resolution
|
||||
) {
|
||||
message.error("请选择比例和分辨率");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
userCredits <
|
||||
calcVideoCredits(
|
||||
record.duration || 5,
|
||||
params.resolution,
|
||||
params.engineId || record.engineId || videoEngines[0]?.id,
|
||||
Boolean(params.includeMediaReferences ?? record.includeMediaReferences),
|
||||
record.references,
|
||||
)
|
||||
) {
|
||||
message.error("积分不足,请先充值");
|
||||
return;
|
||||
}
|
||||
}
|
||||
setGenerating((p) => ({
|
||||
...p,
|
||||
[record.id]: true,
|
||||
}));
|
||||
setRecordStates((p) => ({
|
||||
...p,
|
||||
[record.id]: "generating",
|
||||
}));
|
||||
const mediaText =
|
||||
type === "video" ? "视频" : "图片";
|
||||
message.loading({
|
||||
content: `「${projectName}」正在生成${mediaText}...`,
|
||||
duration: 0,
|
||||
key: record.id,
|
||||
});
|
||||
try {
|
||||
await generateVideo(record.id, {
|
||||
engineId: params?.engineId || record.engineId || (type === 'image' ? imageEngines[0]?.id : videoEngines[0]?.id),
|
||||
includeMediaReferences: Boolean(params?.includeMediaReferences ?? record.includeMediaReferences),
|
||||
aspectRatio: params?.aspectRatio,
|
||||
resolution: params?.resolution,
|
||||
imageSize: record.imageSize,
|
||||
});
|
||||
startPolling(record.id);
|
||||
message.success({
|
||||
content: `「${projectName}」${mediaText}已提交,正在生成中...`,
|
||||
key: record.id,
|
||||
duration: 3,
|
||||
});
|
||||
} catch (error: any) {
|
||||
setRecordStates((p) => ({
|
||||
...p,
|
||||
[record.id]: "failed",
|
||||
}));
|
||||
const errorMsg = error?.response?.data?.message || error?.message || `${mediaText}生成失败`;
|
||||
message.error({
|
||||
content: `「${projectName}」${errorMsg}`,
|
||||
key: record.id,
|
||||
duration: 3,
|
||||
});
|
||||
} finally {
|
||||
setGenerating((p) => ({
|
||||
...p,
|
||||
[record.id]: false,
|
||||
}));
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
fontWeight: 600,
|
||||
color: "#fff",
|
||||
height: 48,
|
||||
background:
|
||||
"linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
|
||||
border: "none",
|
||||
position: "absolute",
|
||||
bottom: 18,
|
||||
left: 0,
|
||||
}}
|
||||
disabled={record.canGenerate === false}
|
||||
onClick={() => handleGenerate(record.id)}
|
||||
style={{ borderRadius: 12, fontWeight: 600, height: 48 }}
|
||||
>
|
||||
生成{type === "video" ? "视频" : "图片"}
|
||||
{type === "video" ? (
|
||||
<span
|
||||
style={{
|
||||
color: "#d2d2d2",
|
||||
fontSize: "14px",
|
||||
marginLeft: "6px",
|
||||
}}
|
||||
>
|
||||
(请选择上方的比例分辨率)
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}{" "}
|
||||
{/* {type === "video" &&
|
||||
historyParams[record.id]?.resolution
|
||||
? `(${calcVideoCredits(
|
||||
record.duration || 5,
|
||||
historyParams[record.id].resolution as Resolution,
|
||||
historyParams[record.id].engineId || record.engineId || videoEngines[0]?.id,
|
||||
Boolean(historyParams[record.id].includeMediaReferences ?? record.includeMediaReferences),
|
||||
record.references,
|
||||
)}积分)`
|
||||
: ""} */}
|
||||
{type === "video" &&
|
||||
historyParams[record.id]?.resolution
|
||||
? `(${calcVideoCredits(
|
||||
record.duration || 5,
|
||||
historyParams[record.id].resolution as Resolution,
|
||||
historyParams[record.id].engineId || record.engineId || videoEngines[0]?.id,
|
||||
Boolean(historyParams[record.id].includeMediaReferences ?? record.includeMediaReferences),
|
||||
record.references,
|
||||
)}积分)`
|
||||
: ""}
|
||||
{type === "image" && record.imageSize
|
||||
? `(${getImageCreditsFromCimage(
|
||||
record.imageSize,
|
||||
historyParams[record.id]?.engineId || record.engineId || imageEngines[0]?.id,
|
||||
Boolean(historyParams[record.id]?.includeMediaReferences ?? record.includeMediaReferences),
|
||||
record.references,
|
||||
)}积分)`
|
||||
: type === "image"
|
||||
? `(积分)`
|
||||
: ""}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -4925,73 +4727,12 @@ const GeneratePage: React.FC = () => {
|
||||
<Typography.Text
|
||||
style={{ color: "#94a3b8", fontSize: 13 }}
|
||||
>
|
||||
{type === "image"
|
||||
? "待生成图片"
|
||||
: status === "prompt_optimized"
|
||||
? "待配置视频参数"
|
||||
: "待生成视频"}
|
||||
{type === "image" ? "待生成图片" : "待生成视频"}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Generate buttons for history records */}
|
||||
{/* {status === 'prompt_optimized' && (
|
||||
<div style={{ marginTop: 16, width: '100%' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 12 }}>
|
||||
<div translate="no" style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '5px 12px', borderRadius: 8, background: '#f8f9fc', border: '1px solid #e2e8f0' }}>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}>时长</Typography.Text>
|
||||
<Typography.Text strong style={{ fontSize: 13, color: '#6366f1' }}>{record.duration}秒</Typography.Text>
|
||||
</div>
|
||||
<PortalDropdown label="比例" value={historyParams[record.id]?.aspectRatio || '选择比例'}
|
||||
options={engineOptions.ratios}
|
||||
expanded={expandedEngine === `ratio-${record.id}`}
|
||||
onToggle={() => setExpandedEngine(expandedEngine === `ratio-${record.id}` ? null : `ratio-${record.id}`)}
|
||||
onSelect={(v) => setHistoryParams(prev => ({ ...prev, [record.id]: { ...prev[record.id], aspectRatio: v as AspectRatio, resolution: prev[record.id]?.resolution || '' } }))}
|
||||
onClose={() => setExpandedEngine(null)}
|
||||
/>
|
||||
<PortalDropdown label="分辨率" value={historyParams[record.id]?.resolution || '选择分辨率'}
|
||||
options={engineOptions.resolutions}
|
||||
expanded={expandedEngine === `res-${record.id}`}
|
||||
onToggle={() => setExpandedEngine(expandedEngine === `res-${record.id}` ? null : `res-${record.id}`)}
|
||||
onSelect={(v) => setHistoryParams(prev => ({ ...prev, [record.id]: { ...prev[record.id], aspectRatio: prev[record.id]?.aspectRatio || '', resolution: v as Resolution } }))}
|
||||
onClose={() => setExpandedEngine(null)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="primary" size="large" icon={<RocketOutlined />} block
|
||||
loading={generating[record.id]}
|
||||
disabled={!historyParams[record.id]?.aspectRatio || !historyParams[record.id]?.resolution}
|
||||
onClick={async () => {
|
||||
const params = historyParams[record.id];
|
||||
if (!params?.aspectRatio || !params?.resolution) { message.error('请选择比例和分辨率'); return; }
|
||||
if (userCredits < calcVideoCredits(record.duration || 5, params.resolution)) { message.error('积分不足,请先充值'); return; }
|
||||
setGenerating((p) => ({ ...p, [record.id]: true }));
|
||||
setRecordStates((p) => ({ ...p, [record.id]: 'generating' }));
|
||||
message.loading({ content: `「${projectName}」正在生成视频...`, duration: 0, key: record.id });
|
||||
try {
|
||||
await generateVideo(record.id, { engineId: params.engineId || record.engineId || videoEngines[0]?.id, includeMediaReferences: Boolean(params.includeMediaReferences ?? record.includeMediaReferences), aspectRatio: params.aspectRatio, resolution: params.resolution, imageSize: record.imageSize });
|
||||
setRecordStates((p) => ({ ...p, [record.id]: 'done' }));
|
||||
message.success({ content: `「${projectName}」视频生成成功!`, key: record.id, duration: 3 });
|
||||
} catch {
|
||||
setRecordStates((p) => ({ ...p, [record.id]: 'failed' }));
|
||||
message.error({ content: `「${projectName}」视频生成失败`, key: record.id, duration: 3 });
|
||||
} finally {
|
||||
setGenerating((p) => ({ ...p, [record.id]: false }));
|
||||
}
|
||||
}}
|
||||
style={{ borderRadius: 12, fontWeight: 600, height: 48, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none' }}>
|
||||
生成视频 {historyParams[record.id]?.resolution ? `(${calcVideoCredits(record.duration || 5, historyParams[record.id].resolution)}积分)` : ''}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{statusState.isFailure && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Button type="primary" danger size="large" icon={<PlayCircleOutlined />} block
|
||||
loading={generating[record.id]}
|
||||
onClick={() => handleRetryGeneration(record.id)}
|
||||
style={{ borderRadius: 12, fontWeight: 600, height: 48 }}>重新生成视频</Button>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -81,6 +81,7 @@ const MessagesPage: React.FC = () => {
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
ellipsis: true,
|
||||
render: (v: string) => <div style={{ maxWidth: 300 }} dangerouslySetInnerHTML={{ __html: v }} />,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Empty,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Pagination,
|
||||
Select,
|
||||
Space,
|
||||
@@ -31,14 +30,14 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import type { AspectRatio, Resolution } from '../types';
|
||||
import { updateRecordPrompt } from '../api';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
import { copyToClipboard } from '../utils/clipboard';
|
||||
import { resolveGenerationUiState } from '../utils/generationTaskStatus';
|
||||
|
||||
const RecordsPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { records, projects, fetchRecords, fetchProjects, generateVideo } = useAppStore();
|
||||
const { records, projects, fetchRecords, fetchProjects, generateVideo, retryGeneration } = useAppStore();
|
||||
const recordItems = records.items;
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
@@ -48,8 +47,33 @@ const RecordsPage: React.FC = () => {
|
||||
const [editablePrompts, setEditablePrompts] = useState<Record<string, string>>({});
|
||||
const [editingRecordId, setEditingRecordId] = useState<string | null>(null);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
// Generate modal for param selection
|
||||
const [genModal, setGenModal] = useState<{ recordId: string; projectName: string; ratio: AspectRatio; resolution: Resolution } | null>(null);
|
||||
const submitFrozenGeneration = async (record: any) => {
|
||||
const type = record.genType || 'image';
|
||||
const retry = record.canRetry === true;
|
||||
if ((!retry && (record.canGenerate === false || !record.configComplete)) || (retry && !record.configComplete)) {
|
||||
message.error('该记录生成配置不完整,请重新生成提词');
|
||||
return;
|
||||
}
|
||||
setGenerating((prev) => ({ ...prev, [record.id]: true }));
|
||||
message.loading({ content: `「${record.projectName}」正在提交${type === 'video' ? '视频' : '图片'}生成...`, duration: 0, key: record.id });
|
||||
try {
|
||||
if (retry) {
|
||||
await retryGeneration(record.id);
|
||||
} else {
|
||||
const editedPrompt = editablePrompts[record.id];
|
||||
if (editedPrompt && editedPrompt !== record.optimizedPrompt) {
|
||||
await updateRecordPrompt(record.id, editedPrompt);
|
||||
}
|
||||
await generateVideo(record.id);
|
||||
}
|
||||
message.success({ content: `「${record.projectName}」已提交,正在生成中...`, key: record.id, duration: 3 });
|
||||
} catch (error: any) {
|
||||
message.error({ content: error?.response?.data?.detail || error?.message || '提交生成失败', key: record.id, duration: 3 });
|
||||
} finally {
|
||||
setGenerating((prev) => ({ ...prev, [record.id]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
@@ -64,48 +88,6 @@ const RecordsPage: React.FC = () => {
|
||||
});
|
||||
}, [fetchRecords, filterProject, filterStatus, currentPage, pageSize]);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!genModal) return;
|
||||
const { recordId, projectName, ratio, resolution } = genModal;
|
||||
setGenerating((p) => ({ ...p, [recordId]: true }));
|
||||
setGenModal(null);
|
||||
message.loading({ content: `「${projectName}」正在生成视频...`, duration: 0, key: recordId });
|
||||
try {
|
||||
await generateVideo(recordId, { aspectRatio: ratio, resolution });
|
||||
message.success({ content: `「${projectName}」视频生成成功!`, key: recordId, duration: 3 });
|
||||
} catch {
|
||||
message.error({ content: `「${projectName}」视频生成失败`, key: recordId, duration: 3 });
|
||||
} finally {
|
||||
setGenerating((p) => ({ ...p, [recordId]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const openGenModal = (record: any) => {
|
||||
const type: any = record.genType || 'image';
|
||||
|
||||
// 如果是图片类型,直接生成,不需要弹窗
|
||||
if (type === 'image') {
|
||||
setGenerating((p) => ({ ...p, [record.id]: true }));
|
||||
message.loading({ content: `「${record.projectName}」正在生成图片...`, duration: 0, key: record.id });
|
||||
generateVideo(record.id, {}).then(() => {
|
||||
message.success({ content: `「${record.projectName}」图片生成成功!`, key: record.id, duration: 3 });
|
||||
}).catch(() => {
|
||||
message.error({ content: `「${record.projectName}」图片生成失败`, key: record.id, duration: 3 });
|
||||
}).finally(() => {
|
||||
setGenerating((p) => ({ ...p, [record.id]: false }));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是视频类型,显示参数选择弹窗
|
||||
setGenModal({
|
||||
recordId: record.id,
|
||||
projectName: record.projectName,
|
||||
ratio: (record.aspectRatio as AspectRatio) || '16:9',
|
||||
resolution: (record.resolution as Resolution) || '720p',
|
||||
});
|
||||
};
|
||||
|
||||
const filtered = recordItems;
|
||||
|
||||
return (
|
||||
@@ -222,7 +204,7 @@ const RecordsPage: React.FC = () => {
|
||||
|
||||
{/* Meta */}
|
||||
{type === 'image' ? <span className="mobile-meta" translate="no" style={{ fontSize: 12, color: '#94a3b8', flexShrink: 0 }}>
|
||||
{record.duration ? `${record.imageSize}` : '-'} · {record.imageProportion || '-'} · {record.imagePx || '-'} · <span className="date-display" translate="no">{formatDate(record.createdAt)}</span>
|
||||
{record.imageSize || '-'} · {record.imageProportion || '-'} · {record.imagePx || '-'} · <span className="date-display" translate="no">{formatDate(record.createdAt)}</span>
|
||||
</span> : <span className="mobile-meta" translate="no" style={{ fontSize: 12, color: '#94a3b8', flexShrink: 0 }}>
|
||||
{record.duration ? `${record.duration}秒` : '-'} · {record.aspectRatio || '-'} · {record.resolution || '-'} · <span className="date-display" translate="no">{formatDate(record.createdAt)}</span>
|
||||
</span>}
|
||||
@@ -233,7 +215,8 @@ const RecordsPage: React.FC = () => {
|
||||
{record.status === 'prompt_optimized' && (
|
||||
<Button type="primary" size="small" icon={<PlayCircleOutlined />}
|
||||
loading={isGenerating}
|
||||
onClick={() => openGenModal(record)}
|
||||
disabled={!record.configComplete || record.canGenerate === false}
|
||||
onClick={() => submitFrozenGeneration(record)}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
@@ -250,7 +233,8 @@ const RecordsPage: React.FC = () => {
|
||||
{uiState.isFailure && (
|
||||
<Button type="primary" danger size="small" icon={<PlayCircleOutlined />}
|
||||
loading={isGenerating}
|
||||
onClick={() => openGenModal(record)}
|
||||
disabled={!record.configComplete || record.canRetry === false}
|
||||
onClick={() => submitFrozenGeneration(record)}
|
||||
style={{ borderRadius: 8 }}>
|
||||
重试
|
||||
</Button>
|
||||
@@ -355,7 +339,7 @@ const RecordsPage: React.FC = () => {
|
||||
{type === 'image' ? [
|
||||
{ label: '分辨率', value: record.imageSize ? `${record.imageSize} ` : '-' },
|
||||
{ label: '画面比例', value: record.imageProportion ? `${record.imageProportion} ` : '-' },
|
||||
{ label: '画面尺寸', value: record.imageProportion ? `${record.imageProportion} ` : '-' },
|
||||
{ label: '画面尺寸', value: record.imagePx ? `${record.imagePx} ` : '-' },
|
||||
{ label: '消耗积分', value: record.creditsCost ? `${record.creditsCost} ` : '-', highlight: !!record.creditsCost },
|
||||
].map((item, j) => (
|
||||
<div key={j} style={{ flex: 1 }}>
|
||||
@@ -364,8 +348,8 @@ const RecordsPage: React.FC = () => {
|
||||
</div>
|
||||
)) : [
|
||||
{ label: '时长', value: record.duration ? `${record.duration} 秒` : '-' },
|
||||
{ label: '画面比例', value: record.aspectRatio || (record.status === 'prompt_optimized' ? '待选择' : '-') },
|
||||
{ label: '分辨率', value: record.resolution || (record.status === 'prompt_optimized' ? '待选择' : '-') },
|
||||
{ label: '画面比例', value: record.aspectRatio || (record.status === 'prompt_optimized' ? '配置缺失' : '-') },
|
||||
{ label: '分辨率', value: record.resolution || (record.status === 'prompt_optimized' ? '配置缺失' : '-') },
|
||||
{ label: '消耗积分', value: record.creditsCost ? `${record.creditsCost}` : '-', highlight: !!record.creditsCost },
|
||||
].map((item, j) => (
|
||||
<div key={j} style={{ flex: 1 }}>
|
||||
@@ -380,13 +364,14 @@ const RecordsPage: React.FC = () => {
|
||||
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button type="primary" size="large" icon={<RocketOutlined />}
|
||||
loading={isGenerating}
|
||||
onClick={() => openGenModal(record)}
|
||||
disabled={!record.configComplete || record.canGenerate === false}
|
||||
onClick={() => submitFrozenGeneration(record)}
|
||||
style={{
|
||||
borderRadius: 12, fontWeight: 600, height: 44,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none', boxShadow: '0 8px 24px rgba(99,102,241,0.3)',
|
||||
}}>
|
||||
{type === 'video' ? '选择参数生成视频' : '生成图片'}
|
||||
{type === 'video' ? '生成视频' : '生成图片'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -396,7 +381,8 @@ const RecordsPage: React.FC = () => {
|
||||
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button type="primary" danger size="large" icon={<PlayCircleOutlined />}
|
||||
loading={isGenerating}
|
||||
onClick={() => openGenModal(record)}
|
||||
disabled={!record.configComplete || record.canRetry === false}
|
||||
onClick={() => submitFrozenGeneration(record)}
|
||||
style={{ borderRadius: 12, fontWeight: 600, height: 44 }}>
|
||||
重新生成{type === 'video' ? '视频' : '图片'}
|
||||
</Button>
|
||||
@@ -509,46 +495,6 @@ const RecordsPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generate modal */}
|
||||
<Modal
|
||||
title={<Space><RocketOutlined />生成视频</Space>}
|
||||
open={!!genModal}
|
||||
onCancel={() => setGenModal(null)}
|
||||
onOk={handleGenerate}
|
||||
okText="提交生成"
|
||||
cancelText="取消"
|
||||
width={420}
|
||||
>
|
||||
{genModal && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 16 }}>
|
||||
{(() => {
|
||||
const rec = recordItems.find(r => r.id === genModal.recordId);
|
||||
return (
|
||||
<>
|
||||
<div style={{ padding: 12, borderRadius: 10, background: '#f8f9fc' }}>
|
||||
<Typography.Text style={{ fontSize: 11, color: '#94a3b8', display: 'block' }}>时长</Typography.Text>
|
||||
<Typography.Text strong>{rec?.duration || 5}秒</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>画面比例</Typography.Text>
|
||||
<Select value={genModal.ratio} onChange={(v) => setGenModal(prev => prev ? { ...prev, ratio: v } : null)}
|
||||
style={{ width: '100%' }}
|
||||
options={['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'].map(r => ({ value: r, label: r }))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>分辨率</Typography.Text>
|
||||
<Select value={genModal.resolution} onChange={(v) => setGenModal(prev => prev ? { ...prev, resolution: v } : null)}
|
||||
style={{ width: '100%' }}
|
||||
options={['480p', '720p', '1080p'].map(r => ({ value: r, label: r }))}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Project, GenerationRecord, OptimizeParams, GenerateParams, OptimizeResult, Industry, MediaReference } from '../types';
|
||||
import type { Project, GenerationRecord, OptimizeParams, OptimizeResult, Industry, MediaReference } from '../types';
|
||||
import * as api from '../api';
|
||||
import { useAuthStore } from './useAuthStore';
|
||||
|
||||
@@ -41,7 +41,8 @@ interface AppState {
|
||||
|
||||
fetchRecords: (params?: api.GetRecordsPageParams) => Promise<void>;
|
||||
optimizePrompt: (projectId: string, params: OptimizeParams) => Promise<OptimizeResult>;
|
||||
generateVideo: (recordId: string, params: GenerateParams) => Promise<GenerationRecord>;
|
||||
generateVideo: (recordId: string) => Promise<GenerationRecord>;
|
||||
retryGeneration: (recordId: string) => Promise<GenerationRecord>;
|
||||
updateRecordReferences: (recordId: string, references: MediaReference[]) => void;
|
||||
|
||||
// 生成配置状态更新方法
|
||||
@@ -126,18 +127,36 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
try { await useAuthStore.getState().checkAuth(); } catch { /* */ }
|
||||
|
||||
const currentRecords = get().records;
|
||||
const existingIndex = currentRecords.items.findIndex((item) => item.id === result.record.id);
|
||||
const nextItems = existingIndex >= 0
|
||||
? currentRecords.items.map((item) => item.id === result.record.id ? result.record : item)
|
||||
: [result.record, ...currentRecords.items];
|
||||
set({
|
||||
records: {
|
||||
...currentRecords,
|
||||
total: currentRecords.total + 1,
|
||||
items: [result.record, ...currentRecords.items],
|
||||
total: existingIndex >= 0 ? currentRecords.total : currentRecords.total + 1,
|
||||
items: nextItems,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
generateVideo: async (recordId, params) => {
|
||||
const record = await api.generateVideo(recordId, params);
|
||||
generateVideo: async (recordId) => {
|
||||
const record = await api.generateVideo(recordId);
|
||||
try { await useAuthStore.getState().checkAuth(); } catch { /* */ }
|
||||
|
||||
const currentRecords = get().records;
|
||||
set({
|
||||
records: {
|
||||
...currentRecords,
|
||||
items: currentRecords.items.map((r) => (r.id === recordId ? record : r)),
|
||||
},
|
||||
});
|
||||
return record;
|
||||
},
|
||||
|
||||
retryGeneration: async (recordId) => {
|
||||
const record = await api.retryGeneration(recordId);
|
||||
try { await useAuthStore.getState().checkAuth(); } catch { /* */ }
|
||||
|
||||
const currentRecords = get().records;
|
||||
|
||||
@@ -181,7 +181,7 @@ export interface GenerationRecord {
|
||||
originalPrompt: string;
|
||||
optimizedPrompt?: string;
|
||||
duration?: number;
|
||||
genType?: number;
|
||||
genType?: 'video' | 'image';
|
||||
aspectRatio?: AspectRatio;
|
||||
resolution?: Resolution;
|
||||
status: GenerationStatus;
|
||||
@@ -197,38 +197,37 @@ export interface GenerationRecord {
|
||||
errorMessage?: string;
|
||||
createdAt: string;
|
||||
generatedAt?: string;
|
||||
imageSize: string;
|
||||
imageProportion: string;
|
||||
imagePx: string;
|
||||
imageUrl: string;
|
||||
imageSize?: string;
|
||||
imageProportion?: string;
|
||||
imagePx?: string;
|
||||
imageUrl?: string;
|
||||
engineId?: string;
|
||||
engineName?: string;
|
||||
engineSnapshot?: Record<string, any>;
|
||||
includeMediaReferences?: boolean;
|
||||
configComplete?: boolean;
|
||||
configRecoverable?: boolean;
|
||||
configFallbackHint?: string;
|
||||
canGenerate?: boolean;
|
||||
canRetry?: boolean;
|
||||
shouldPoll?: boolean;
|
||||
clientStatus?: 'prompt_processing' | 'ready' | 'generating' | 'success' | 'failure';
|
||||
operationPhase?: 'prompt' | 'resource';
|
||||
}
|
||||
|
||||
export interface OptimizeParams {
|
||||
prompt: string;
|
||||
duration: number;
|
||||
genType?: any;
|
||||
duration?: number;
|
||||
genType: 'video' | 'image';
|
||||
engineId: string;
|
||||
includeMediaReferences?: boolean;
|
||||
resolution?: string;
|
||||
aspectRatio?: string;
|
||||
references?: MediaReference[];
|
||||
idempotencyKey?: string;
|
||||
image_size?: any;
|
||||
image_proportion?: any;
|
||||
image_px?: any;
|
||||
video_duration?: number;
|
||||
video_ratio?: string;
|
||||
video_resolution?: string;
|
||||
}
|
||||
|
||||
export interface GenerateParams {
|
||||
engineId?: string;
|
||||
includeMediaReferences?: boolean;
|
||||
aspectRatio?: AspectRatio;
|
||||
resolution?: Resolution;
|
||||
imageSize?: string;
|
||||
image_size?: string;
|
||||
image_proportion?: string;
|
||||
image_px?: string;
|
||||
}
|
||||
|
||||
export type ModuleGenerationFlowVersion = 'v1' | 'v2';
|
||||
|
||||
@@ -2,6 +2,10 @@ export interface GenerationStatusLike {
|
||||
status?: string | null;
|
||||
displayStatus?: string | null;
|
||||
pipelineStage?: string | null;
|
||||
clientStatus?: string | null;
|
||||
shouldPoll?: boolean | null;
|
||||
canGenerate?: boolean | null;
|
||||
canRetry?: boolean | null;
|
||||
}
|
||||
|
||||
export type GenerationUiColor = 'default' | 'processing' | 'warning' | 'success' | 'error' | 'blue' | 'orange' | 'purple';
|
||||
@@ -17,42 +21,34 @@ export interface GenerationUiState {
|
||||
isSuccess: boolean;
|
||||
isFailure: boolean;
|
||||
isTerminal: boolean;
|
||||
canGenerate: boolean;
|
||||
canRetry: boolean;
|
||||
shouldPoll: boolean;
|
||||
}
|
||||
|
||||
const ACTIVE_STATUS_KEYS = new Set([
|
||||
'pending',
|
||||
'optimizing',
|
||||
'prompt_optimized',
|
||||
'generating',
|
||||
]);
|
||||
const CLIENT_STATUS_MAP: Record<string, string> = {
|
||||
prompt_processing: 'optimizing',
|
||||
ready: 'prompt_optimized',
|
||||
generating: 'generating',
|
||||
success: 'completed',
|
||||
failure: 'failed',
|
||||
};
|
||||
|
||||
const ACTIVE_STATUS_KEYS = new Set(['pending', 'optimizing', 'generating']);
|
||||
const ACTIVE_PIPELINE_STAGES = new Set([
|
||||
'queued',
|
||||
'preparing',
|
||||
'creating_provider_task',
|
||||
'provider_result_staged',
|
||||
'waiting_remote',
|
||||
'polling',
|
||||
'result_ready',
|
||||
'download_queued',
|
||||
'downloading',
|
||||
'retry_waiting',
|
||||
'upscale_queued',
|
||||
'upscale_processing',
|
||||
'upscale_polling',
|
||||
'upscale_downloading',
|
||||
'upscale_finalizing',
|
||||
'upscale_retry_waiting',
|
||||
'queued', 'preparing', 'creating_provider_task', 'provider_result_staged',
|
||||
'waiting_remote', 'polling', 'result_ready', 'download_queued', 'downloading',
|
||||
'retry_waiting', 'recovery_inconsistent', 'upscale_queued', 'upscale_processing',
|
||||
'upscale_polling', 'upscale_downloading', 'upscale_finalizing', 'upscale_retry_waiting',
|
||||
]);
|
||||
|
||||
const SUCCESS_KEYS = new Set(['completed', 'done']);
|
||||
const FAILURE_KEYS = new Set(['failed', 'timeout', 'download_failed', 'upscale_failed']);
|
||||
const TERMINAL_KEYS = new Set([...SUCCESS_KEYS, ...FAILURE_KEYS, 'deleted']);
|
||||
|
||||
const LABELS: Record<string, string> = {
|
||||
pending: '生成中',
|
||||
optimizing: '生成中',
|
||||
prompt_optimized: '生成中',
|
||||
pending: '待处理',
|
||||
optimizing: '提词处理中',
|
||||
prompt_optimized: '待生成',
|
||||
generating: '生成中',
|
||||
queued: '生成中',
|
||||
preparing: '生成中',
|
||||
@@ -64,15 +60,16 @@ const LABELS: Record<string, string> = {
|
||||
download_queued: '生成中',
|
||||
downloading: '生成中',
|
||||
retry_waiting: '生成中',
|
||||
recovery_inconsistent: '生成中',
|
||||
upscale_queued: '生成中',
|
||||
upscale_processing: '生成中',
|
||||
upscale_polling: '生成中',
|
||||
upscale_downloading: '生成中',
|
||||
upscale_finalizing: '生成中',
|
||||
upscale_retry_waiting: '生成中',
|
||||
completed: '已完成',
|
||||
done: '已完成',
|
||||
timeout: '生成超时',
|
||||
completed: '成功',
|
||||
done: '成功',
|
||||
timeout: '失败',
|
||||
download_failed: '失败',
|
||||
upscale_failed: '失败',
|
||||
failed: '失败',
|
||||
@@ -80,33 +77,15 @@ const LABELS: Record<string, string> = {
|
||||
};
|
||||
|
||||
const COLOR_MAP: Record<string, GenerationUiColor> = {
|
||||
pending: 'default',
|
||||
optimizing: 'processing',
|
||||
prompt_optimized: 'processing',
|
||||
generating: 'warning',
|
||||
queued: 'processing',
|
||||
preparing: 'processing',
|
||||
creating_provider_task: 'processing',
|
||||
provider_result_staged: 'processing',
|
||||
waiting_remote: 'processing',
|
||||
polling: 'processing',
|
||||
result_ready: 'processing',
|
||||
download_queued: 'processing',
|
||||
downloading: 'processing',
|
||||
retry_waiting: 'orange',
|
||||
upscale_queued: 'purple',
|
||||
upscale_processing: 'purple',
|
||||
upscale_polling: 'purple',
|
||||
upscale_downloading: 'purple',
|
||||
upscale_finalizing: 'purple',
|
||||
upscale_retry_waiting: 'orange',
|
||||
completed: 'success',
|
||||
done: 'success',
|
||||
failed: 'error',
|
||||
timeout: 'error',
|
||||
download_failed: 'error',
|
||||
upscale_failed: 'error',
|
||||
deleted: 'default',
|
||||
pending: 'default', optimizing: 'processing', prompt_optimized: 'blue', generating: 'processing',
|
||||
queued: 'processing', preparing: 'processing', creating_provider_task: 'processing',
|
||||
provider_result_staged: 'processing', waiting_remote: 'processing', polling: 'processing',
|
||||
result_ready: 'processing', download_queued: 'processing', downloading: 'processing',
|
||||
retry_waiting: 'orange', recovery_inconsistent: 'orange', upscale_queued: 'purple',
|
||||
upscale_processing: 'purple', upscale_polling: 'purple', upscale_downloading: 'purple',
|
||||
upscale_finalizing: 'purple', upscale_retry_waiting: 'orange', completed: 'success',
|
||||
done: 'success', failed: 'error', timeout: 'error', download_failed: 'error',
|
||||
upscale_failed: 'error', deleted: 'default',
|
||||
};
|
||||
|
||||
const normalize = (value?: string | null): string => String(value || '').trim().toLowerCase();
|
||||
@@ -121,45 +100,28 @@ export const getGenerationStatusColor = (key?: string | null): GenerationUiColor
|
||||
return COLOR_MAP[normalized] || 'default';
|
||||
};
|
||||
|
||||
const firstMatching = (values: string[], keys: Set<string>): string => (
|
||||
values.find((value) => keys.has(value)) || ''
|
||||
);
|
||||
|
||||
export const resolveGenerationUiState = (value: GenerationStatusLike): GenerationUiState => {
|
||||
const status = normalize(value.status);
|
||||
const displayStatus = normalize(value.displayStatus);
|
||||
const pipelineStage = normalize(value.pipelineStage);
|
||||
const values = [displayStatus, status, pipelineStage].filter(Boolean);
|
||||
const clientStatus = normalize(value.clientStatus);
|
||||
const mappedClientStatus = CLIENT_STATUS_MAP[clientStatus] || '';
|
||||
|
||||
const failureKey = FAILURE_KEYS.has(pipelineStage)
|
||||
? pipelineStage
|
||||
: firstMatching([displayStatus, status], FAILURE_KEYS);
|
||||
const deletedKey = firstMatching(values, new Set(['deleted']));
|
||||
const successKey = firstMatching(values, SUCCESS_KEYS);
|
||||
|
||||
let effectiveKey = '';
|
||||
if (failureKey) {
|
||||
effectiveKey = failureKey;
|
||||
} else if (deletedKey) {
|
||||
effectiveKey = deletedKey;
|
||||
} else if (ACTIVE_PIPELINE_STAGES.has(pipelineStage)) {
|
||||
let effectiveKey = mappedClientStatus || displayStatus || status || pipelineStage || 'pending';
|
||||
if (FAILURE_KEYS.has(pipelineStage) || FAILURE_KEYS.has(status) || FAILURE_KEYS.has(displayStatus)) {
|
||||
effectiveKey = FAILURE_KEYS.has(pipelineStage) ? pipelineStage : (FAILURE_KEYS.has(status) ? status : displayStatus);
|
||||
} else if (SUCCESS_KEYS.has(status) || SUCCESS_KEYS.has(displayStatus)) {
|
||||
effectiveKey = SUCCESS_KEYS.has(status) ? status : displayStatus;
|
||||
} else if (!mappedClientStatus && pipelineStage && ACTIVE_PIPELINE_STAGES.has(pipelineStage)) {
|
||||
effectiveKey = pipelineStage;
|
||||
} else if (successKey) {
|
||||
effectiveKey = successKey;
|
||||
} else if (pipelineStage) {
|
||||
effectiveKey = pipelineStage;
|
||||
} else {
|
||||
effectiveKey = displayStatus || status || 'pending';
|
||||
}
|
||||
|
||||
const isFailure = FAILURE_KEYS.has(effectiveKey);
|
||||
const isSuccess = SUCCESS_KEYS.has(effectiveKey);
|
||||
const isActive = !isFailure && !isSuccess && effectiveKey !== 'deleted' && (
|
||||
ACTIVE_PIPELINE_STAGES.has(pipelineStage)
|
||||
|| ACTIVE_STATUS_KEYS.has(displayStatus)
|
||||
|| ACTIVE_STATUS_KEYS.has(status)
|
||||
|| ACTIVE_PIPELINE_STAGES.has(effectiveKey)
|
||||
);
|
||||
const shouldPoll = typeof value.shouldPoll === 'boolean'
|
||||
? value.shouldPoll
|
||||
: (!isFailure && !isSuccess && (ACTIVE_STATUS_KEYS.has(status) || ACTIVE_PIPELINE_STAGES.has(pipelineStage)));
|
||||
const isActive = shouldPoll;
|
||||
|
||||
return {
|
||||
status,
|
||||
@@ -172,6 +134,9 @@ export const resolveGenerationUiState = (value: GenerationStatusLike): Generatio
|
||||
isSuccess,
|
||||
isFailure,
|
||||
isTerminal: TERMINAL_KEYS.has(effectiveKey),
|
||||
canGenerate: typeof value.canGenerate === 'boolean' ? value.canGenerate : status === 'prompt_optimized',
|
||||
canRetry: typeof value.canRetry === 'boolean' ? value.canRetry : isFailure,
|
||||
shouldPoll,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user