AI创作批量生成任务 main V1 init
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { Empty, Spin, Tag, Typography } from 'antd';
|
||||
import { PlayCircleFilled } from '@ant-design/icons';
|
||||
import type { GenerationAITaskOut } from '../../types';
|
||||
|
||||
interface Props {
|
||||
task: GenerationAITaskOut;
|
||||
resolveUrl: (url?: string | null) => string;
|
||||
onPreview: (url: string, type: 'image' | 'video', title: string) => void;
|
||||
}
|
||||
|
||||
const spanByCount = (count: number, index: number): number => {
|
||||
if (count <= 1) return 6;
|
||||
if (count === 2 || count === 4) return 3;
|
||||
if (count === 3) return index < 2 ? 3 : 6;
|
||||
return index < 3 ? 2 : 3;
|
||||
};
|
||||
|
||||
const LABELS: Record<string, string> = {
|
||||
pending: '待处理', queued: '已入队', preparing: '准备中', generating: '生成中',
|
||||
creating_provider_task: '创建任务中', waiting_remote: '等待生成', polling: '轮询中',
|
||||
result_ready: '结果就绪', download_queued: '等待下载', downloading: '下载中',
|
||||
retry_waiting: '等待重试', completed: '已完成', failed: '生成失败',
|
||||
download_failed: '下载失败', deleted: '已删除',
|
||||
};
|
||||
|
||||
const GenerationTaskResourceGrid: React.FC<Props> = ({ task, resolveUrl, onPreview }) => {
|
||||
const count = Math.max(1, Math.min(5, Number(task.generationCount || task.childItems?.length || 1)));
|
||||
const sortedChildren = [...(task.childItems || [])].sort((a, b) => Number(a.generationIndex || 0) - Number(b.generationIndex || 0));
|
||||
const items: GenerationAITaskOut[] = sortedChildren.length
|
||||
? sortedChildren
|
||||
: (count > 1
|
||||
? Array.from({ length: count }, (_, index) => ({ ...task, id: `${task.id}-${index + 1}`, generationIndex: index + 1, childItems: [] }))
|
||||
: [task]);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: 430, display: 'grid', gridTemplateColumns: 'repeat(6, minmax(0,1fr))', gridAutoRows: 'minmax(0,1fr)', gap: items.length > 1 ? 8 : 0 }}>
|
||||
{items.map((item, index) => {
|
||||
const status = item.displayStatus || item.pipelineStage || item.status || 'pending';
|
||||
const isVideo = item.genType === 'video';
|
||||
const resultUrl = resolveUrl(isVideo ? item.videoUrl : item.imageUrl);
|
||||
const coverUrl = resolveUrl(item.videoCoverUrl);
|
||||
const active = ['pending', 'queued', 'preparing', 'generating', 'creating_provider_task', 'waiting_remote', 'polling', 'result_ready', 'download_queued', 'downloading', 'retry_waiting'].includes(status);
|
||||
return (
|
||||
<div key={item.id} style={{ gridColumn: `span ${spanByCount(items.length, index)}`, minWidth: 0, minHeight: 0, border: '1px solid #edf0f5', borderRadius: 10, overflow: 'hidden', position: 'relative', background: '#f8f9fc' }}>
|
||||
{resultUrl && status !== 'deleted' ? (
|
||||
<button type="button" onClick={() => onPreview(resultUrl, isVideo ? 'video' : 'image', `生成结果 ${item.generationIndex || index + 1}`)} style={{ width: '100%', height: '100%', padding: 0, border: 0, background: 'transparent', cursor: 'pointer', position: 'relative' }}>
|
||||
{isVideo ? (coverUrl ? <img src={coverUrl} alt="视频封面" style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> : <video src={resultUrl} muted preload="metadata" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />) : <img src={resultUrl} alt="生成图片" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />}
|
||||
{isVideo ? <PlayCircleFilled style={{ position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%,-50%)', color: '#fff', fontSize: 38, filter: 'drop-shadow(0 3px 8px rgba(0,0,0,.35))' }} /> : null}
|
||||
</button>
|
||||
) : (
|
||||
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 9, padding: 12, textAlign: 'center' }}>
|
||||
{active ? <Spin size="small" /> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={null} />}
|
||||
<Tag color={status === 'deleted' ? 'default' : (status === 'download_failed' || status === 'failed' ? 'error' : 'processing')}>{LABELS[status] || status}</Tag>
|
||||
{item.errorMessage && !active ? <Typography.Text type="danger" style={{ fontSize: 11 }}>{item.errorMessage}</Typography.Text> : null}
|
||||
</div>
|
||||
)}
|
||||
{items.length > 1 ? <span style={{ position: 'absolute', top: 6, left: 6, padding: '1px 7px', borderRadius: 10, color: '#fff', background: 'rgba(17,24,39,.58)', fontSize: 11 }}>#{item.generationIndex || index + 1}</span> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerationTaskResourceGrid;
|
||||
@@ -32,6 +32,7 @@ import dayjs from 'dayjs';
|
||||
import { getAdminGenerationAiTasks } from '../api';
|
||||
import type { GenerationAIMediaReference, GenerationAITaskOut } from '../types';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
import GenerationTaskResourceGrid from '../components/generation/GenerationTaskResourceGrid';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
@@ -71,6 +72,8 @@ const STATUS_MAP: Record<string, { color: string; text: string; icon: React.Reac
|
||||
generating: { color: 'warning', text: '生成中', icon: <LoadingOutlined spin /> },
|
||||
completed: { color: 'success', text: '已完成', icon: <CheckCircleOutlined /> },
|
||||
failed: { color: 'error', text: '失败', icon: <CloseCircleOutlined /> },
|
||||
download_failed: { color: 'error', text: '下载失败', icon: <CloseCircleOutlined /> },
|
||||
deleted: { color: 'default', text: '已删除', icon: <CloseCircleOutlined /> },
|
||||
};
|
||||
|
||||
const PIPELINE_STAGE_MAP: Record<string, string> = {
|
||||
@@ -425,6 +428,25 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
return <Tag color={cfg.color} icon={cfg.icon}>{cfg.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '生成数量', key: 'generationCount', width: 150,
|
||||
render: (_: any, r: GenerationAITaskOut) => {
|
||||
const count = Math.max(1, Number(r.generationCount || 1));
|
||||
if (count === 1) return <Tag>1份</Tag>;
|
||||
const children = r.childItems || [];
|
||||
const completed = children.filter((item) => (item.displayStatus || item.status) === 'completed').length;
|
||||
const failed = children.filter((item) => ['failed', 'download_failed'].includes(item.displayStatus || item.status)).length;
|
||||
const deleted = children.filter((item) => (item.displayStatus || item.status) === 'deleted').length;
|
||||
return (
|
||||
<Space size={4} wrap>
|
||||
<Tag color="purple">{count}份</Tag>
|
||||
<Typography.Text style={{ fontSize: 11, color: '#64748b' }}>
|
||||
{completed}完成{failed ? ` / ${failed}失败` : ''}{deleted ? ` / ${deleted}删除` : ''}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '引擎', key: 'engine', width: 160,
|
||||
render: (_: any, r: GenerationAITaskOut) => {
|
||||
@@ -1045,6 +1067,7 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
<InfoItem label="用户名称" value={preview.userName || '未知用户'} />
|
||||
<InfoItem label="用户ID" value={preview.userId || '-'} />
|
||||
<InfoItem label="任务ID" value={preview.id} />
|
||||
<InfoItem label="生成数量" value={`${preview.generationCount || 1} 份`} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -1122,38 +1145,16 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{preview.status === 'completed' ? (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#94a3b8', display: 'block' }}>
|
||||
{preview.genType === 'video' ? '生成视频' : '生成图片'}
|
||||
</Typography.Text>
|
||||
{preview.genType === 'video' && preview.videoUrl ? (
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => handlePreviewResource(preview.videoUrl!, 'video', '生成视频')}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
弹窗播放
|
||||
</Button>
|
||||
) : null}
|
||||
{preview.genType === 'image' && preview.imageUrl ? (
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
icon={<FileImageOutlined />}
|
||||
onClick={() => handlePreviewResource(preview.imageUrl!, 'image', '生成图片')}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
弹窗查看
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{preview.genType === 'video' ? renderResultVideo() : renderResultImage()}
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#94a3b8', display: 'block', marginBottom: 6 }}>
|
||||
生成资源(共 {preview.generationCount || 1} 份)
|
||||
</Typography.Text>
|
||||
<GenerationTaskResourceGrid
|
||||
task={preview}
|
||||
resolveUrl={apiUrl}
|
||||
onPreview={handlePreviewResource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{preview.status === 'failed' && preview.errorMessage ? (
|
||||
<div style={{ padding: 12, borderRadius: 10, background: 'rgba(239,68,68,0.04)', border: '1px solid rgba(239,68,68,0.15)' }}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Checkbox, Form, Input, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography,
|
||||
Button, Card, Checkbox, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
PictureOutlined, PlusOutlined, EditOutlined, DeleteOutlined,
|
||||
@@ -21,6 +21,11 @@ interface ImageEngine {
|
||||
generateUrl: string;
|
||||
isActive: boolean;
|
||||
priority: number;
|
||||
multiGenerationEnabled: boolean;
|
||||
maxGenerationCount: number;
|
||||
multiImageMaxImages: number;
|
||||
maxReferenceImageCount: number;
|
||||
outputFormat: '' | 'png' | 'jpeg';
|
||||
}
|
||||
|
||||
function parseJsonArray(val: unknown): any[] {
|
||||
@@ -80,6 +85,7 @@ const AdminImageEngines: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modal, setModal] = useState<{ open: boolean; engine: ImageEngine | null }>({ open: false, engine: null });
|
||||
const [form] = Form.useForm();
|
||||
const multiGenerationEnabled = Form.useWatch('multiGenerationEnabled', form) ?? false;
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
@@ -126,6 +132,11 @@ const AdminImageEngines: React.FC = () => {
|
||||
generate_url: values.generateUrl || '',
|
||||
is_active: values.isActive ?? true,
|
||||
priority: values.priority ?? 0,
|
||||
multi_generation_enabled: values.multiGenerationEnabled ?? false,
|
||||
max_generation_count: values.maxGenerationCount ?? 1,
|
||||
multi_image_max_images: values.multiImageMaxImages ?? 15,
|
||||
max_reference_image_count: values.maxReferenceImageCount ?? 14,
|
||||
output_format: values.outputFormat ?? '',
|
||||
};
|
||||
if (modal.engine) {
|
||||
await saveImageEngine({ id: modal.engine.id, ...payload });
|
||||
@@ -168,6 +179,8 @@ const AdminImageEngines: React.FC = () => {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
isActive: true, priority: 0,
|
||||
multiGenerationEnabled: false, maxGenerationCount: 1, multiImageMaxImages: 15,
|
||||
maxReferenceImageCount: 14, outputFormat: '',
|
||||
supportedModels: ['doubao-seedream-5-0-260128'],
|
||||
defaultSize: '2K',
|
||||
maxImageCount: 0,
|
||||
@@ -232,6 +245,18 @@ const AdminImageEngines: React.FC = () => {
|
||||
title: '最大图片', dataIndex: 'maxImageCount', width: 100,
|
||||
render: (v: number) => <Tag color="purple">{v} 张</Tag>,
|
||||
},
|
||||
{
|
||||
title: '多份生成', dataIndex: 'multiGenerationEnabled', width: 100,
|
||||
render: (v: boolean) => <Tag color={v ? 'blue' : 'default'}>{v ? '开启' : '关闭'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '数量上限', dataIndex: 'maxGenerationCount', width: 100,
|
||||
render: (v: number, r: ImageEngine) => (
|
||||
<Tag color={r.multiGenerationEnabled && Number(v || 1) > 1 ? 'magenta' : 'default'}>
|
||||
最多 {r.multiGenerationEnabled ? (v || 1) : 1} 份
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'isActive', width: 80,
|
||||
render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '启用' : '停用'}</Tag>,
|
||||
@@ -350,6 +375,33 @@ const AdminImageEngines: React.FC = () => {
|
||||
<Form.Item name="generateUrl" label="生成接口地址">
|
||||
<Input placeholder="https://ark.cn-beijing.volces.com/api/v3/images/generations" size="large" />
|
||||
</Form.Item>
|
||||
<div style={{ background: '#f8f9fc', borderRadius: 10, padding: 16, marginBottom: 12 }}>
|
||||
<Typography.Text strong>多份生成能力</Typography.Text>
|
||||
<Typography.Paragraph style={{ margin: '6px 0 0', color: '#64748b', fontSize: 12 }}>
|
||||
管理后台只控制是否允许客户端选择多份及最大数量。客户端本次选择 2-5 份时,后端只调用一次火山同步组图 API;失败绝不降级成多次单图请求。
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 16 }}>
|
||||
<Form.Item name="multiGenerationEnabled" label="允许客户端多份生成" valuePropName="checked">
|
||||
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
|
||||
</Form.Item>
|
||||
<Form.Item name="maxGenerationCount" label="客户端最大生成数量" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={5} precision={0} size="large" style={{ width: '100%' }} disabled={!multiGenerationEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item name="multiImageMaxImages" label="组图输入输出总上限" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={15} precision={0} size="large" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="maxReferenceImageCount" label="最大参考图数量" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={14} precision={0} size="large" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="outputFormat" label="供应商输出格式">
|
||||
<Select size="large" options={[
|
||||
{ value: '', label: '不传(兼容不支持 output_format 的模型)' },
|
||||
{ value: 'png', label: 'PNG' },
|
||||
{ value: 'jpeg', label: 'JPEG' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item name="priority" label="优先级">
|
||||
<Select size="large" options={[
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Form, Input, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography,
|
||||
Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlayCircleOutlined, PlusOutlined, EditOutlined, DeleteOutlined,
|
||||
@@ -25,6 +25,8 @@ interface VideoEngine {
|
||||
supportsUniversalReference: boolean;
|
||||
isActive: boolean;
|
||||
priority: number;
|
||||
multiGenerationEnabled: boolean;
|
||||
maxGenerationCount: number;
|
||||
}
|
||||
|
||||
function parseJsonArray(val: unknown): any[] {
|
||||
@@ -40,6 +42,7 @@ const AdminVideoEngines: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modal, setModal] = useState<{ open: boolean; engine: VideoEngine | null }>({ open: false, engine: null });
|
||||
const [form] = Form.useForm();
|
||||
const multiGenerationEnabled = Form.useWatch('multiGenerationEnabled', form) ?? false;
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
@@ -80,6 +83,8 @@ const AdminVideoEngines: React.FC = () => {
|
||||
supports_universal_reference: values.supportsUniversalReference ?? true,
|
||||
is_active: values.isActive ?? true,
|
||||
priority: values.priority ?? 0,
|
||||
multi_generation_enabled: values.multiGenerationEnabled ?? false,
|
||||
max_generation_count: values.maxGenerationCount ?? 1,
|
||||
};
|
||||
if (modal.engine) {
|
||||
await saveVideoEngine({ id: modal.engine.id, ...payload });
|
||||
@@ -115,6 +120,7 @@ const AdminVideoEngines: React.FC = () => {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
isActive: true, priority: 0,
|
||||
multiGenerationEnabled: false, maxGenerationCount: 1,
|
||||
maxDuration: 30,
|
||||
maxImageCount: 2,
|
||||
maxVideoCount: 0,
|
||||
@@ -180,6 +186,18 @@ const AdminVideoEngines: React.FC = () => {
|
||||
title: '全能参考', dataIndex: 'supportsUniversalReference', width: 100,
|
||||
render: (v: boolean) => <Tag color={v ? 'purple' : 'default'}>{v ? '支持' : '不支持'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '多份生成', dataIndex: 'multiGenerationEnabled', width: 100,
|
||||
render: (v: boolean) => <Tag color={v ? 'blue' : 'default'}>{v ? '开启' : '关闭'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '数量上限', dataIndex: 'maxGenerationCount', width: 100,
|
||||
render: (v: number, r: VideoEngine) => (
|
||||
<Tag color={r.multiGenerationEnabled && Number(v || 1) > 1 ? 'magenta' : 'default'}>
|
||||
最多 {r.multiGenerationEnabled ? (v || 1) : 1} 份
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'isActive', width: 80,
|
||||
render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '启用' : '停用'}</Tag>,
|
||||
@@ -315,7 +333,19 @@ const AdminVideoEngines: React.FC = () => {
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{ background: '#f8f9fc', borderRadius: 10, padding: 16, marginBottom: 12 }}>
|
||||
<Typography.Text strong>多份生成能力</Typography.Text>
|
||||
<Typography.Paragraph style={{ margin: '6px 0 0', color: '#64748b', fontSize: 12 }}>
|
||||
管理后台只控制是否允许客户端选择多份及最大数量;客户端每次可在 1 到上限之间选择。
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item name="multiGenerationEnabled" label="允许客户端多份生成" valuePropName="checked" style={{ flex: 1 }}>
|
||||
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
|
||||
</Form.Item>
|
||||
<Form.Item name="maxGenerationCount" label="客户端最大生成数量" style={{ flex: 1 }} rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={5} precision={0} size="large" style={{ width: '100%' }} disabled={!multiGenerationEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item name="priority" label="优先级" style={{ flex: 1 }}>
|
||||
<Select size="large" options={[
|
||||
{ value: 0, label: '0 (默认)' },
|
||||
|
||||
@@ -281,6 +281,10 @@ export interface GenerationAiImageEngine {
|
||||
supportedSizes: Record<string, Record<string, string>>;
|
||||
defaultSize: string;
|
||||
priority: number;
|
||||
multiGenerationEnabled: boolean;
|
||||
maxGenerationCount: number;
|
||||
multiImageMaxImages: number;
|
||||
maxReferenceImageCount: number;
|
||||
}
|
||||
|
||||
export interface GenerationAiVideoEngine {
|
||||
@@ -299,6 +303,8 @@ export interface GenerationAiVideoEngine {
|
||||
supportsFirstLastFrame?: boolean;
|
||||
supportsUniversalReference?: boolean;
|
||||
priority: number;
|
||||
multiGenerationEnabled: boolean;
|
||||
maxGenerationCount: number;
|
||||
}
|
||||
|
||||
export interface GenerationAiEnginesResponse {
|
||||
@@ -326,6 +332,10 @@ export interface GenerationAiEngineOption {
|
||||
supportsFirstLastFrame?: boolean;
|
||||
supportsUniversalReference?: boolean;
|
||||
priority: number;
|
||||
multiGenerationEnabled?: boolean;
|
||||
maxGenerationCount?: number;
|
||||
multiImageMaxImages?: number;
|
||||
maxReferenceImageCount?: number;
|
||||
genType: GenerationAiGenType;
|
||||
}
|
||||
|
||||
@@ -399,6 +409,10 @@ export interface GenerationAITaskOut {
|
||||
projectId?: string | null;
|
||||
genType: GenerationAiGenType | string;
|
||||
generationMode?: string | null;
|
||||
parentTaskId?: string | null;
|
||||
generationCount: number;
|
||||
generationIndex?: number | null;
|
||||
displayStatus?: string | null;
|
||||
pipelineStage?: string | null;
|
||||
status: GenerationAITaskStatus;
|
||||
originalPrompt: string;
|
||||
@@ -428,6 +442,7 @@ export interface GenerationAITaskOut {
|
||||
errorMessage?: string | null;
|
||||
createdAt?: string | null;
|
||||
generatedAt?: string | null;
|
||||
childItems: GenerationAITaskOut[];
|
||||
}
|
||||
|
||||
export interface GenerationAITaskListOut {
|
||||
|
||||
Reference in New Issue
Block a user