ai页面修改

This commit is contained in:
sjy
2026-07-02 18:10:56 +08:00
parent b39dae6d55
commit 9edaeb4031
9 changed files with 2185 additions and 2160 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-DzSXW83p.js"></script>
<script type="module" crossorigin src="/assets/index-_JKe0xeY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bi8mcSs8.css">
</head>
<body>
+4 -1
View File
@@ -110,7 +110,10 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
// Handle error responses
if (!res.ok) {
const msg = parsed?.detail || `请求失败 (${res.status})`;
let msg = parsed?.detail?.message || parsed?.message || `请求失败 (${res.status})`;
if (typeof parsed?.detail === 'string') {
msg = parsed.detail;
}
if (res.status === 401 && !options.skipAuthRedirect) {
clearToken();
window.location.href = '/login';
@@ -160,23 +160,18 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
</div>
{data.enabled && (
<>
<div
style={{
width: '100%',
height: 20,
background: '#e2e8f0',
borderRadius: 2,
overflow: 'hidden',
position: 'relative',
marginTop: 4,
display: 'flex',
justifyContent: 'space-between',
fontSize: 11,
color: isOver ? '#ef4444' : '#94a3b8',
}}
>
<div style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
background: 'transparent',
width: '100%',
display: 'flex',
alignItems: 'center',
@@ -202,6 +197,26 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
)}
</div>
{/* <span>
{isOver
? `存储超额 · 超用 ${Math.abs(available).toFixed(2)} ${unit}`
: `剩余 ${available.toFixed(2)} ${unit}`}
</span> */}
{/* <span>{rawPercent.toFixed(1)}%</span> */}
</div>
<div
style={{
width: '100%',
height: 6,
background: '#e2e8f0',
borderRadius: 50,
overflow: 'hidden',
position: 'relative',
}}
>
<div
style={{
@@ -219,22 +234,6 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
/>
</div>
<div
style={{
marginTop: 4,
display: 'flex',
justifyContent: 'space-between',
fontSize: 11,
color: isOver ? '#ef4444' : '#94a3b8',
}}
>
{/* <span>
{isOver
? `存储超额 · 超用 ${Math.abs(available).toFixed(2)} ${unit}`
: `剩余 ${available.toFixed(2)} ${unit}`}
</span> */}
{/* <span>{rawPercent.toFixed(1)}%</span> */}
</div>
</>
)}
</div>
@@ -896,8 +895,8 @@ const AppLayout: React.FC = () => {
fontSize: 16,
fontWeight: 500,
letterSpacing: 0,
background: 'rgba(99, 102, 241, 0.08)',
padding: '2px 8px',
// background: 'rgba(99, 102, 241, 0.08)',
// padding: '2px 8px',
borderRadius: 6,
display: 'inline-block',
}}>: {user?.credits || 0}</div>
+102 -79
View File
@@ -64,7 +64,7 @@ const { Text } = Typography;
interface MediaReference {
name: string;
type: 'image' | 'video';
type: 'image' | 'video' | 'audio';
url: string;
duration?: number;
role?: string;
@@ -203,16 +203,20 @@ const AIChatPage: React.FC = () => {
};
// 根据媒体列表生成标签(图片一、图片二、视频一、视频二...)
const generateMediaLabels = (media: { type: 'image' | 'video' }[]) => {
const generateMediaLabels = (media: { type: 'image' | 'video' | 'audio' }[]) => {
let imgCount = 0;
let vidCount = 0;
let audCount = 0;
return media.map((m) => {
if (m.type === 'image') {
imgCount++;
return `图片${imgCount}`;
} else {
} else if (m.type === 'video') {
vidCount++;
return `视频${vidCount}`;
} else {
audCount++;
return `音频${audCount}`;
}
});
};
@@ -502,7 +506,7 @@ const AIChatPage: React.FC = () => {
}
}
let supportedSizes = (data as any).engine?.image?.[0]?.supported_sizes || {};
let supportedSizes = (data as any).engine?.image?.[0]?.supportedSizes || {};
let supportedResolutions: string[] = [];
let twokwidth: string[] = [];
let fourkwidth: string[] = [];
@@ -721,6 +725,7 @@ const AIChatPage: React.FC = () => {
const handleRemoveFirstFrame = () => {
setFirstFrame(null);
setLastFrame(null);
};
const handleRemoveLastFrame = () => {
@@ -977,7 +982,7 @@ const AIChatPage: React.FC = () => {
});
};
const handleUpload = async (file: File, frameTarget?: 'first' | 'last') => {
const handleUpload = async (file: File, frameTarget?: 'first' | 'last' | File[]) => {
const isImage = file.type.startsWith('image/');
const isVideo = file.type.startsWith('video/');
@@ -1020,8 +1025,10 @@ const AIChatPage: React.FC = () => {
return false;
}
if (!isImage && !isVideo) {
message.error('仅支持图片或视频文件');
const isAudio = file.type.startsWith('audio/');
if (!isImage && !isVideo && !isAudio) {
message.error('仅支持图片、视频或音频文件');
return false;
}
@@ -1030,14 +1037,15 @@ const AIChatPage: React.FC = () => {
const maxImage = currentEngine?.maxImageCount ?? 4;
const maxVideo = currentEngine?.maxVideoCount ?? 1;
if (mediaType === 'image' && isVideo) {
if (mediaType === 'image' && (isVideo || isAudio)) {
message.error('图片模式仅支持上传图片');
return false;
}
const maxMB = isVideo ? 100 : 10;
const maxMB = isVideo ? 100 : (isAudio ? 50 : 10);
const fileTypeText = isVideo ? '视频' : (isAudio ? '音频' : '图片');
if (file.size / 1024 / 1024 > maxMB) {
message.error(`${isVideo ? '视频' : '图片'}大小不能超过${maxMB}MB`);
message.error(`${fileTypeText}大小不能超过${maxMB}MB`);
return false;
}
@@ -1077,19 +1085,19 @@ const AIChatPage: React.FC = () => {
setUploading(true);
try {
const uploadFn = isImage ? uploadImage : uploadVideo;
const uploadFn = isImage ? uploadImage : (isAudio ? uploadImage : uploadVideo);
const res = await uploadFn(file);
const mediaType: 'image' | 'video' = isImage ? 'image' : 'video';
const mediaType: 'image' | 'video' | 'audio' = isImage ? 'image' : (isAudio ? 'audio' : 'video');
const newList = [...currentMedia, {
name: file.name,
type: mediaType,
url: res.url,
label: '',
...(isVideo && { duration: videoDuration }),
...((isVideo || isAudio) && { duration: videoDuration }),
}];
const labels = generateMediaLabels(newList);
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
message.success(`${isImage ? '图片' : '视频'}上传成功`);
message.success(`${isImage ? '图片' : (isAudio ? '音频' : '视频')}上传成功`);
} catch (error) {
message.error('上传失败');
} finally {
@@ -1247,7 +1255,7 @@ const AIChatPage: React.FC = () => {
? '输入画面描述,或上传图片参考风格、构图、主体与光影。例:@图片1 参考色调,生成一张简洁高级的产品海报。'
: isFirstLastFrameComposer
? '描述首帧到尾帧的主体动作、镜头运动、节奏与转场。例:从首帧自然推进到尾帧,产品居中突出,动作平滑连贯。'
: '上传最多12个参考素材,输入文字或 @ 引用参考内容,自由组合图、文、音、视频。例:@图片1 模仿 @视频1 的动作,音色参考 @音频1。';
: '上传最多12个参考素材,输入文字或 @ 引用参考内容,自由组合图、文、视频。例:@图片1 模仿 @视频1 的动作。';
const composerHelperText = mediaType === 'image'
? '图片参考 · 适合海报、产品图、场景图与风格图生成'
: isFirstLastFrameComposer
@@ -1259,11 +1267,8 @@ const AIChatPage: React.FC = () => {
margin: '-24px -32px -32px',
borderRadius: 22,
height: 'calc(100vh - 34px)',
background: 'linear-gradient(180deg, #ffffff 0%, #ffffff 100%)',
backgroundRepeat: 'no-repeat',
backgroundSize: '100% 100%',
backgroundPosition: 'center',
backgroundBlendMode: 'normal',
background: '#fff',
overflow: 'hidden',
}}>
{/* 左侧边栏 - 对话列表(已隐藏,保留代码) */}
@@ -1353,15 +1358,15 @@ const AIChatPage: React.FC = () => {
)}
{/* 主内容区 */}
<Layout style={{ display: 'flex', flex: 1, background: 'transparent' }}>
<Layout style={{ display: 'flex', flex: 1, background: '#fff' }}>
{/* 头部 - 显示对话标题和模型信息 */}
<div className="animate-fadeInUp" style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
marginBottom: 18, padding: '16px 24px', borderRadius: 22,
background: 'rgba(255, 255, 255, 0.92)',
padding: '16px 24px', borderRadius: 22,
background: 'rgb(255, 255, 255)',
backdropFilter: 'blur(22px)',
border: '1px solid rgba(231, 234, 240, 0.82)' ,
boxShadow: '0 16px 44px rgba(31, 41, 55, 0.06)',
border: '1px solid rgba(231, 234, 240, 0.82)',
// boxShadow: '0 16px 44px rgba(31, 41, 55, 0.06)',
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
}}>
<div style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 16 }}>
@@ -1434,8 +1439,10 @@ const AIChatPage: React.FC = () => {
overflowY: 'auto',
paddingRight: 10,
paddingBottom: 18,
background: 'linear-gradient(180deg, rgba(255,255,255,0.72), rgba(255,255,255,0))',
borderRadius: 22,
// background: 'linear-gradient(180deg, rgba(255,255,255,0.72), rgba(255,255,255,0))',
background: '#fff',
// borderRadius: 22,
}}
>
{/* 加载更多按钮 - 在列表顶部 */}
@@ -1483,7 +1490,7 @@ const AIChatPage: React.FC = () => {
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
boxShadow: '0 8px 18px rgba(47, 52, 64, 0.15)' ,
// boxShadow: '0 8px 18px rgba(47, 52, 64, 0.15)' ,
}}
>
<RobotOutlined style={{ color: '#fff', fontSize: 16 }} />
@@ -1506,7 +1513,7 @@ const AIChatPage: React.FC = () => {
boxSizing: 'border-box',
boxShadow: '0 18px 50px rgba(47, 52, 64, 0.075)',
position: 'relative',
border: '1px solid rgba(231, 234, 240, 0.9)' ,
border: '1px solid rgba(231, 234, 240, 0.9)',
}}
>
@@ -1696,7 +1703,8 @@ const AIChatPage: React.FC = () => {
{/* 根据 status 显示不同内容 */}
{(msg.status === 'generating' || msg.status === 'failed' || msg.status === 'completed') && (
<div style={{ display: 'flex', gap: 16, marginBottom: 16, marginTop: 16, width: '100%', alignItems: 'flex-start',
<div style={{
display: 'flex', gap: 16, marginBottom: 16, marginTop: 16, width: '100%', alignItems: 'flex-start',
}}>
<div style={{ width: '50%', overflow: 'hidden', borderRadius: 12, position: 'relative', height: 220, border: '1px solid #E7EAF0', boxShadow: '0 4px 12px rgba(139, 92, 246, 0.08)', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'linear-gradient(135deg, #ffffff 0%, #FAFBFC 100%)' }}>
@@ -1746,19 +1754,22 @@ const AIChatPage: React.FC = () => {
</div>
<div style={{ width: '50%', display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ background: '#FFFFFF', borderRadius: 12, padding: 12, border: '1px solid #E7EAF0', flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<p style={{width: '100%' ,height: 150, overflow: 'auto', padding: 0, marginTop: 0, marginBottom: 0, lineHeight: 2, color: '#475467', fontSize: 13 }}>{msg.originalPrompt}</p>
<p style={{ width: '100%', height: 150, overflow: 'auto', padding: 0, marginTop: 0, marginBottom: 0, lineHeight: 2, color: '#475467', fontSize: 13 }}>{msg.originalPrompt}</p>
</div>
<div style={{ fontSize: 12, color: '#667085', textAlign: 'left', display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'center', justifyContent: 'space-between',marginBottom: 12 }}>
<div style={{ fontSize: 12, color: '#667085', textAlign: 'left', display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<span style={{
// background: 'rgba(139, 92, 246, 0.08)',
borderRadius: 16, color: '#8b5cf6', fontWeight: 500 }}>{msg.engineSnapshot.name}</span>
borderRadius: 16, color: '#8b5cf6', fontWeight: 500
}}>{msg.engineSnapshot.name}</span>
<span style={{
// background: 'rgba(139, 92, 246, 0.08)',
borderRadius: 16, color: '#667085' }}>{msg.genType === 'image' ? `${msg.imageProportion || ''} · ${msg.imagePx || ''} · ${msg.imageSize || ''}` : `${msg.duration || ''}s · ${msg.aspectRatio || ''} · ${msg.resolution || ''}`}</span>
borderRadius: 16, color: '#667085'
}}>{msg.genType === 'image' ? `${msg.imageProportion || ''} · ${msg.imagePx || ''} · ${msg.imageSize || ''}` : `${msg.duration || ''}s · ${msg.aspectRatio || ''} · ${msg.resolution || ''}`}</span>
<span style={{
// background: 'rgba(164, 91, 91, 0.08)',
fontSize: 14,
borderRadius: 16, color: '#8b5cf6' }}>{msg.creditsCost}</span>
borderRadius: 16, color: '#8b5cf6'
}}>{msg.creditsCost}</span>
</div>
@@ -1828,7 +1839,7 @@ const AIChatPage: React.FC = () => {
<span style={{ fontSize: 11, color: '#98A2B3', fontWeight: 600 }}></span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 11, color: '#98A2B3', fontWeight: 700 }}>{attachmentRefs.length}/12</span>
{/* <span style={{ fontSize: 11, color: '#98A2B3', fontWeight: 700 }}>{attachmentRefs.length}/12</span> */}
<button
onClick={() => {
setAttachmentPopupVisible(false);
@@ -1917,9 +1928,9 @@ const AIChatPage: React.FC = () => {
</div>
</>
)}
<span style={{ position: 'absolute', left: 6, top: 6, padding: '2px 6px', borderRadius: 999, background: 'rgba(255,255,255,0.92)', color: '#8b5cf6', fontSize: 10, fontWeight: 800, boxShadow: '0 4px 10px rgba(31, 41, 55, 0.08)' }}>
{/* <span style={{ position: 'absolute', left: 6, top: 6, padding: '2px 6px', borderRadius: 999, background: 'rgba(255,255,255,0.92)', color: '#8b5cf6', fontSize: 10, fontWeight: 800, boxShadow: '0 4px 10px rgba(31, 41, 55, 0.08)' }}>
{label}
</span>
</span> */}
</div>
<div title={ref.name || label} style={{ marginTop: 6, color: '#667085', fontSize: 10, fontWeight: 700, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }}>
@@ -1997,18 +2008,17 @@ const AIChatPage: React.FC = () => {
{/* 上方输入布局 */}
<div style={{ display: 'flex', gap: isFirstLastFrameComposer ? 18 : 18, alignItems: 'flex-start', marginBottom: 14 }}>
{/* 左侧附件区域 */}
<div style={{ width: isFirstLastFrameComposer ? 376 : 128, minWidth: isFirstLastFrameComposer ? 376 : 128, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-start', paddingTop: 2 }}>
<div style={{ width: isFirstLastFrameComposer ? 240 : 128, minWidth: isFirstLastFrameComposer ? 240 : 128, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-start', paddingTop: 2 }}>
{/* 首尾帧模式 */}
{mediaType === 'video' && referenceMode === 'first_last_frame' ? (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'stretch', gap: 10, width: '100%', height: 92 }}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'stretch', gap: 8, width: '100%', height: 76 }}>
{/* 首帧 */}
<div style={{ position: 'relative', flex: 1, minWidth: 0, height: 92, borderRadius: 16, border: '1px solid #E7EAF0', background: '#ffffff', boxShadow: '0 8px 20px rgba(47, 52, 64, 0.045)', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 8, left: 10, right: 10, display: 'flex', alignItems: 'center', justifyContent: 'space-between', zIndex: 2 }}>
<span style={{ fontSize: 11, fontWeight: 700, color: '#8b5cf6', lineHeight: 1 }}></span>
<span style={{ fontSize: 10, color: '#98A2B3' }}></span>
<div style={{ position: 'relative', flex: 1, minWidth: 0, height: 76, borderRadius: 12, border: '1px solid #E7EAF0', background: '#ffffff', boxShadow: '0 6px 16px rgba(47, 52, 64, 0.04)', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 6, left: 8, right: 8, display: 'flex', alignItems: 'center', justifyContent: 'space-between', zIndex: 2 }}>
<span style={{ fontSize: 10, fontWeight: 600, color: '#8b5cf6', lineHeight: 1 }}></span>
</div>
{firstFrame ? (
<div style={{ position: 'absolute', left: 10, right: 10, bottom: 10, height: 54 }}>
<div style={{ position: 'absolute', left: 8, right: 8, bottom: 8, height: 44 }}>
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${firstFrame.url}`}
alt={firstFrame.name}
@@ -2018,24 +2028,24 @@ const AIChatPage: React.FC = () => {
setAttachmentPreviewName(firstFrame.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid rgba(139, 92, 246, 0.2)', boxShadow: '0 4px 12px rgba(47, 52, 64, 0.08)' }}
style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: 8, cursor: 'pointer', border: '1px solid rgba(139, 92, 246, 0.2)', boxShadow: '0 3px 10px rgba(47, 52, 64, 0.06)' }}
/>
<button
onClick={handleRemoveFirstFrame}
style={{ position: 'absolute', top: -7, right: -7, width: 20, height: 20, border: 'none', background: '#A45B5B', borderRadius: 50, cursor: 'pointer', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 2px 6px rgba(164, 91, 91, 0.35)', zIndex: 10 }}
style={{ position: 'absolute', top: -6, right: -6, width: 18, height: 18, border: 'none', background: '#A45B5B', borderRadius: 50, cursor: 'pointer', color: '#fff', fontSize: 9, display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 2px 4px rgba(164, 91, 91, 0.3)', zIndex: 10 }}
>
<DeleteOutlined style={{ fontSize: 10 }} />
<DeleteOutlined style={{ fontSize: 9 }} />
</button>
</div>
) : (
<Upload accept="image/*" showUploadList={false} beforeUpload={(file) => handleUpload(file, 'first')}>
<div
style={{ position: 'absolute', left: 10, right: 10, bottom: 10, height: 54, borderRadius: 10, border: '1px dashed rgba(139, 92, 246, 0.28)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.25s ease', backgroundColor: 'rgba(139, 92, 246, 0.04)', flexDirection: 'column', gap: 3 }}
style={{ position: 'absolute', left: 8, right: 8, bottom: 8, height: 44, borderRadius: 8, border: '1px dashed rgba(139, 92, 246, 0.28)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.25s ease', backgroundColor: 'rgba(139, 92, 246, 0.04)', flexDirection: 'column', gap: 2 }}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = '#a78bfa'; e.currentTarget.style.backgroundColor = 'rgba(139, 92, 246, 0.08)'; }}
onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'rgba(139, 92, 246, 0.28)'; e.currentTarget.style.backgroundColor = 'rgba(139, 92, 246, 0.04)'; }}
>
<PlusOutlined style={{ fontSize: 16, color: '#8b5cf6' }} />
<span style={{ fontSize: 11, color: '#8b5cf6', fontWeight: 600 }}></span>
<PlusOutlined style={{ fontSize: 14, color: '#8b5cf6' }} />
<span style={{ fontSize: 10, color: '#8b5cf6', fontWeight: 500 }}></span>
</div>
</Upload>
)}
@@ -2046,21 +2056,20 @@ const AIChatPage: React.FC = () => {
onClick={handleSwapFrames}
disabled={!firstFrame || !lastFrame}
title="调换首尾帧"
style={{ width: 34, height: 34, alignSelf: 'center', borderRadius: 50, border: '1px solid rgba(231, 234, 240, 0.95)', background: firstFrame && lastFrame ? 'linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%)' : '#F3F5F8', cursor: firstFrame && lastFrame ? 'pointer' : 'not-allowed', color: firstFrame && lastFrame ? '#fff' : '#98A2B3', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: firstFrame && lastFrame ? '0 8px 18px rgba(139, 92, 246, 0.28)' : 'none', transition: 'all 0.2s ease', flexShrink: 0 }}
style={{ width: 28, height: 28, alignSelf: 'center', borderRadius: 50, border: '1px solid rgba(231, 234, 240, 0.95)', background: firstFrame && lastFrame ? 'linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%)' : '#F3F5F8', cursor: firstFrame && lastFrame ? 'pointer' : 'not-allowed', color: firstFrame && lastFrame ? '#fff' : '#98A2B3', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: firstFrame && lastFrame ? '0 6px 14px rgba(139, 92, 246, 0.24)' : 'none', transition: 'all 0.2s ease', flexShrink: 0 }}
onMouseEnter={(e) => { if (firstFrame && lastFrame) e.currentTarget.style.transform = 'scale(1.06)'; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}
>
<SwapOutlined style={{ fontSize: 14 }} />
<SwapOutlined style={{ fontSize: 12 }} />
</button>
{/* 尾帧 */}
<div style={{ position: 'relative', flex: 1, minWidth: 0, height: 92, borderRadius: 16, border: '1px solid #E7EAF0', background: '#ffffff', boxShadow: '0 8px 20px rgba(47, 52, 64, 0.045)', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 8, left: 10, right: 10, display: 'flex', alignItems: 'center', justifyContent: 'space-between', zIndex: 2 }}>
<span style={{ fontSize: 11, fontWeight: 700, color: '#8b5cf6', lineHeight: 1 }}></span>
<span style={{ fontSize: 10, color: '#98A2B3' }}></span>
<div style={{ position: 'relative', flex: 1, minWidth: 0, height: 76, borderRadius: 12, border: '1px solid #E7EAF0', background: '#ffffff', boxShadow: '0 6px 16px rgba(47, 52, 64, 0.04)', overflow: 'hidden', opacity: !firstFrame ? 0.5 : 1 }}>
<div style={{ position: 'absolute', top: 6, left: 8, right: 8, display: 'flex', alignItems: 'center', justifyContent: 'space-between', zIndex: 2 }}>
<span style={{ fontSize: 10, fontWeight: 600, color: '#8b5cf6', lineHeight: 1 }}></span>
</div>
{lastFrame ? (
<div style={{ position: 'absolute', left: 10, right: 10, bottom: 10, height: 54 }}>
<div style={{ position: 'absolute', left: 8, right: 8, bottom: 8, height: 44 }}>
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${lastFrame.url}`}
alt={lastFrame.name}
@@ -2070,29 +2079,38 @@ const AIChatPage: React.FC = () => {
setAttachmentPreviewName(lastFrame.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid rgba(139, 92, 246, 0.2)', boxShadow: '0 4px 12px rgba(47, 52, 64, 0.08)' }}
style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: 8, cursor: 'pointer', border: '1px solid rgba(139, 92, 246, 0.2)', boxShadow: '0 3px 10px rgba(47, 52, 64, 0.06)' }}
/>
<button
onClick={handleRemoveLastFrame}
style={{ position: 'absolute', top: -7, right: -7, width: 20, height: 20, border: 'none', background: '#A45B5B', borderRadius: 50, cursor: 'pointer', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 2px 6px rgba(164, 91, 91, 0.35)', zIndex: 10 }}
style={{ position: 'absolute', top: -6, right: -6, width: 18, height: 18, border: 'none', background: '#A45B5B', borderRadius: 50, cursor: 'pointer', color: '#fff', fontSize: 9, display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 2px 4px rgba(164, 91, 91, 0.3)', zIndex: 10 }}
>
<DeleteOutlined style={{ fontSize: 10 }} />
<DeleteOutlined style={{ fontSize: 9 }} />
</button>
</div>
) : (
firstFrame ? (
<Upload accept="image/*" showUploadList={false} beforeUpload={(file) => handleUpload(file, 'last')}>
<div
style={{ position: 'absolute', left: 10, right: 10, bottom: 10, height: 54, borderRadius: 10, border: '1px dashed rgba(139, 92, 246, 0.24)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.25s ease', backgroundColor: 'rgba(139, 92, 246, 0.04)', flexDirection: 'column', gap: 3 }}
style={{ position: 'absolute', left: 8, right: 8, bottom: 8, height: 44, borderRadius: 8, border: '1px dashed rgba(139, 92, 246, 0.24)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.25s ease', backgroundColor: 'rgba(139, 92, 246, 0.04)', flexDirection: 'column', gap: 2 }}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = '#a78bfa'; e.currentTarget.style.backgroundColor = 'rgba(139, 92, 246, 0.08)'; }}
onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'rgba(139, 92, 246, 0.24)'; e.currentTarget.style.backgroundColor = 'rgba(139, 92, 246, 0.04)'; }}
>
<PlusOutlined style={{ fontSize: 16, color: '#8b5cf6' }} />
<span style={{ fontSize: 11, color: '#8b5cf6', fontWeight: 600 }}></span>
<PlusOutlined style={{ fontSize: 14, color: '#8b5cf6' }} />
<span style={{ fontSize: 10, color: '#8b5cf6', fontWeight: 500 }}></span>
</div>
</Upload>
) : (
<div
style={{ position: 'absolute', left: 8, right: 8, bottom: 8, height: 44, borderRadius: 8, border: '1px dashed rgba(226, 232, 240, 0.4)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'not-allowed', backgroundColor: 'rgba(241, 245, 249, 0.5)', flexDirection: 'column', gap: 2 }}
>
<PlusOutlined style={{ fontSize: 14, color: '#cbd5e1' }} />
<span style={{ fontSize: 10, color: '#94a3b8', fontWeight: 500 }}></span>
</div>
)
)}
</div>
</div> ) : (
</div>) : (
<>
{currentMedia.length > 0 ? (
<div
@@ -2215,7 +2233,7 @@ const AIChatPage: React.FC = () => {
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
<span style={{ fontSize: 12, color: '#344054', fontWeight: 800 }}></span>
<span style={{ fontSize: 11, color: '#98A2B3', fontWeight: 700 }}>{currentMedia.length}/12</span>
<span style={{ fontSize: 11, color: '#98A2B3', fontWeight: 700 }}>{currentMedia.length}/{maxImageCount + maxVideoCount}</span>
</div>
<div className="ai-reference-tray-scroll" style={{ display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 2 }}>
{currentMedia.map((media, idx) => (
@@ -2250,7 +2268,7 @@ const AIChatPage: React.FC = () => {
</span>
<button
onClick={() => handleRemoveMedia(idx)}
style={{ position: 'absolute', top: -6, right: -6, width: 18, height: 18, border: '1px solid rgba(255,255,255,0.9)', background: '#A45B5B', borderRadius: 50, cursor: 'pointer', color: '#fff', fontSize: 9, display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 4px 10px rgba(164, 91, 91, 0.28)', zIndex: 10 }}
style={{ position: 'absolute', top: 0, right: -6, width: 18, height: 18, border: '1px solid rgba(255,255,255,0.9)', background: '#A45B5B', borderRadius: 50, cursor: 'pointer', color: '#fff', fontSize: 9, display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 4px 10px rgba(164, 91, 91, 0.28)', zIndex: 10 }}
>
<DeleteOutlined style={{ fontSize: 9 }} />
</button>
@@ -2266,7 +2284,7 @@ const AIChatPage: React.FC = () => {
{!(mediaType === 'video' && referenceMode === 'first_last_frame') && (
<div style={{ marginTop: currentMedia.length > 0 ? 8 : 0, width: '100%', display: 'flex', justifyContent: 'center', minHeight: currentMedia.length > 0 ? 82 : 84 }}>
<Upload
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*'}
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
showUploadList={false}
beforeUpload={handleUpload}
>
@@ -2313,7 +2331,7 @@ const AIChatPage: React.FC = () => {
<span style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}><span></span><span></span></span>
<span style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}><span></span><span></span></span>
</div>
<span style={{ fontSize: 9, color: '#98a2b3', fontWeight: 600, lineHeight: 1 }}>{currentMedia.length}/12</span>
{/* <span style={{ fontSize: 9, color: '#98a2b3', fontWeight: 600, lineHeight: 1 }}>{currentMedia.length}/{maxImageCount + maxVideoCount}</span> */}
</>
)}
</div>
@@ -2341,12 +2359,12 @@ const AIChatPage: React.FC = () => {
position: 'relative',
minHeight: isFirstLastFrameComposer ? 92 : 74,
}}>
{!inputValue.trim() && (
{/* {!inputValue.trim() && (
<div style={{ display: 'flex', alignItems: 'center', gap: 10, minHeight: 22, paddingTop: 1 }}>
<span style={{ fontSize: 13, fontWeight: 800, color: '#475467', letterSpacing: 0.1, whiteSpace: 'nowrap' }}>{composerModeLabel}</span>
<span style={{ fontSize: 13, color: '#667085', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: 500 }}>{composerHelperText}</span>
</div>
)}
)} */}
{/* 文本输入框 */}
<TextArea
ref={mentionInputRef}
@@ -2369,7 +2387,8 @@ const AIChatPage: React.FC = () => {
fontWeight: 500,
resize: 'none',
backgroundColor: 'transparent',
padding: inputValue.trim() ? '4px 0 0' : '2px 0 0',
padding: 10,
}}
disabled={loading}
/>
@@ -2434,19 +2453,23 @@ const AIChatPage: React.FC = () => {
{/* 底部工具栏 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 14, marginTop: 10, paddingTop: 13, borderTop: '1px solid rgba(231, 234, 240, 0.76)' }}>
<Space size={8} wrap style={{ flex: 1, minWidth: 0 }}>
<span>
</span>
{/* 文件类型选择器 */}
<Select
className="ai-composer-select"
value={mediaType}
onChange={(val) => setMediaType(val)}
style={{
width: 118,
minWidth: 118,
padding: '6px 14px',
height: 32,
outline: 'none',
border: '1px solid rgba(226, 233, 244, 0.95)',
backgroundColor: 'rgba(255,255,255,0.86)',
borderRadius: 11,
boxShadow: '0 4px 12px rgba(47, 52, 64, 0.055)',
border: '1px solid #E7EAF0',
backgroundColor: '#ffffff',
borderRadius: 10,
boxShadow: '0 1px 2px rgba(47, 52, 64, 0.025)',
}}
size="middle"
>
@@ -2463,7 +2486,7 @@ const AIChatPage: React.FC = () => {
{/* 引擎选择器 */}
<div style={{ position: 'relative', display: 'inline-block' }}>
<button
onClick={() => setShowEngineModal(true)}
onClick={() => setShowEngineModal(prev => !prev)}
className="image-settings-trigger"
style={{
minWidth: 174,
@@ -2581,7 +2604,7 @@ const AIChatPage: React.FC = () => {
}}
>
<span style={{
fontSize: 13,
fontSize: 14,
fontWeight: countType === engine.id ? 600 : 500,
color: countType === engine.id ? '#8b5cf6' : '#475467',
marginBottom: 2,
+3 -3
View File
@@ -392,7 +392,7 @@ function InitialInfo() {
const createvideo = (stepId: number, engineId: string) => {
let params = {
engine_id: engineId,
engine_id: '',
}
@@ -404,11 +404,11 @@ function InitialInfo() {
}
const agincreatevideo = () => {
let params = {
engine_id: steps[3].engineId,
engine_id: '',
}
getfour(taskDetail.id, steps[2].id.toString(), params).then((res: any) => {
getfour(taskDetail.id, steps[3].id.toString(), params).then((res: any) => {
// 重新获取任务详情以更新数据
refreshTaskDetail();
}).catch((error: any) => {
+3 -3
View File
@@ -397,7 +397,7 @@ function InitialInfo() {
const createvideo = (stepId: number, engineId: string) => {
let params = {
engine_id: engineId,
engine_id: '',
}
@@ -409,11 +409,11 @@ function InitialInfo() {
}
const agincreatevideo = () => {
let params = {
engine_id: steps[3].engineId,
engine_id: '',
}
removefour(taskDetail.id, steps[2].id.toString(), params).then((res: any) => {
removefour(taskDetail.id, steps[3].id.toString(), params).then((res: any) => {
// 重新获取任务详情以更新数据
refreshTaskDetail();
}).catch((error: any) => {
+2 -2
View File
@@ -32,7 +32,7 @@ interface AppState {
};
enginesele: any;
inputValue: string;
currentMedia: { name: string; type: 'image' | 'video'; url: string; label: string; duration?: number }[];
currentMedia: { name: string; type: 'image' | 'video' | 'audio'; url: string; label: string; duration?: number }[];
fetchProjects: () => Promise<void>;
createProject: (name: string, industry: Industry) => Promise<Project>;
@@ -59,7 +59,7 @@ interface AppState {
setVideoResolution: (resolution: string) => void;
setEnginesele: (enginesele: any) => void;
setInputValue: (inputValue: string) => void;
setCurrentMedia: (currentMedia: { name: string; type: 'image' | 'video'; url: string; label: string; duration?: number }[]) => void;
setCurrentMedia: (currentMedia: { name: string; type: 'image' | 'video' | 'audio'; url: string; label: string; duration?: number }[]) => void;
resetGenerationConfig: () => void;
}
+1 -1
View File
@@ -71,7 +71,7 @@ export type GenerationStatus =
export interface MediaReference {
url: string;
type: 'image' | 'video';
type: 'image' | 'video' | 'audio';
name: string;
}