ai对话修改

This commit is contained in:
sjy
2026-07-03 11:46:13 +08:00
parent 28b1d24173
commit 778a89cabb
3 changed files with 610 additions and 484 deletions
File diff suppressed because one or more lines are too long
+36 -36
View File
@@ -1,37 +1,37 @@
<!doctype html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>民众智创</title> <title>民众智创</title>
<script> <script>
(function() { (function() {
var cached = localStorage.getItem('siteInfo'); var cached = localStorage.getItem('siteInfo');
if (cached) { if (cached) {
try { try {
var info = JSON.parse(cached); var info = JSON.parse(cached);
if (info.siteName) { if (info.siteName) {
document.title = info.siteName; document.title = info.siteName;
} }
if (info.siteLogo) { if (info.siteLogo) {
var link = document.querySelector('link[rel="icon"]'); var link = document.querySelector('link[rel="icon"]');
if (link) { if (link) {
link.href = info.siteLogo; link.href = info.siteLogo;
link.type = 'image/png'; link.type = 'image/png';
} }
} }
} catch (e) {} } catch (e) {}
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-DxvrbZVm.js"></script> <script type="module" crossorigin src="/assets/index-BRAajPcM.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D9_3MPsN.css"> <link rel="stylesheet" crossorigin href="/assets/index-D9_3MPsN.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>
+484 -358
View File
@@ -50,6 +50,7 @@ import {
ArrowUpOutlined, ArrowUpOutlined,
DownloadOutlined, DownloadOutlined,
ReloadOutlined, ReloadOutlined,
AudioOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
@@ -274,7 +275,7 @@ const AIChatPage: React.FC = () => {
// 附件预览弹窗状态(独立弹窗) // 附件预览弹窗状态(独立弹窗)
const [attachmentPreviewVisible, setAttachmentPreviewVisible] = useState<boolean>(false); const [attachmentPreviewVisible, setAttachmentPreviewVisible] = useState<boolean>(false);
const [attachmentPreviewUrl, setAttachmentPreviewUrl] = useState<string>(''); const [attachmentPreviewUrl, setAttachmentPreviewUrl] = useState<string>('');
const [attachmentPreviewType, setAttachmentPreviewType] = useState<'image' | 'video'>('image'); const [attachmentPreviewType, setAttachmentPreviewType] = useState<'image' | 'video' | 'audio'>('image');
const [attachmentPreviewName, setAttachmentPreviewName] = useState<string>(''); const [attachmentPreviewName, setAttachmentPreviewName] = useState<string>('');
const attachmentPreviewVideoRef = useRef<HTMLVideoElement>(null); const attachmentPreviewVideoRef = useRef<HTMLVideoElement>(null);
@@ -292,6 +293,7 @@ const AIChatPage: React.FC = () => {
const [fblindex, setFBlindex] = useState<any>(0); const [fblindex, setFBlindex] = useState<any>(0);
const [showEngineModal, setShowEngineModal] = useState(false); const [showEngineModal, setShowEngineModal] = useState(false);
const [showMediaTypeModal, setShowMediaTypeModal] = useState(false);
const [creditRatios, setCreditRatios] = useState<any[]>([]); const [creditRatios, setCreditRatios] = useState<any[]>([]);
const [cimage, setCimage] = useState<any[]>([]); const [cimage, setCimage] = useState<any[]>([]);
@@ -428,6 +430,10 @@ const AIChatPage: React.FC = () => {
useEffect(() => { useEffect(() => {
const handleClickOutside = (e: MouseEvent) => { const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
// 关闭媒体类型选择弹窗
if (showMediaTypeModal && !target.closest('.image-settings-popover') && !target.closest('.image-settings-trigger')) {
setShowMediaTypeModal(false);
}
// 关闭引擎选择弹窗 // 关闭引擎选择弹窗
if (showEngineModal && !target.closest('.image-settings-popover') && !target.closest('.image-settings-trigger')) { if (showEngineModal && !target.closest('.image-settings-popover') && !target.closest('.image-settings-trigger')) {
setShowEngineModal(false); setShowEngineModal(false);
@@ -446,7 +452,7 @@ const AIChatPage: React.FC = () => {
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('mousedown', handleClickOutside);
}; };
}, [showEngineModal, showImageSettingsModal, showVideoSettingsModal]); }, [showMediaTypeModal, showEngineModal, showImageSettingsModal, showVideoSettingsModal]);
useEffect(() => { useEffect(() => {
if (mediaType !== 'video') return; if (mediaType !== 'video') return;
@@ -788,12 +794,8 @@ const AIChatPage: React.FC = () => {
const isFirstLastFrameMode = mediaType === 'video' && referenceMode === 'first_last_frame'; const isFirstLastFrameMode = mediaType === 'video' && referenceMode === 'first_last_frame';
if (isFirstLastFrameMode) { if (isFirstLastFrameMode) {
if (!inputValue.trim() && !firstFrame) { if (!inputValue.trim()) {
message.warning('请输入内容或上传首帧图片'); message.warning('请输入内容');
return;
}
if (!firstFrame) {
message.warning('请上传首帧图片');
return; return;
} }
} else { } else {
@@ -2094,152 +2096,7 @@ const AIChatPage: React.FC = () => {
<div style={{ display: 'flex', gap: isFirstLastFrameComposer ? 18 : 18, alignItems: 'flex-end', marginBottom: 14 }}> <div style={{ display: 'flex', gap: isFirstLastFrameComposer ? 18 : 18, alignItems: 'flex-end', marginBottom: 14 }}>
{/* 左侧附件区域 */} {/* 左侧附件区域 */}
<div style={{ width: isFirstLastFrameComposer ? 220 : 70, minWidth: isFirstLastFrameComposer ? 220 : 70, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-start', paddingTop: 2, gap: 8 }}> <div style={{ width: isFirstLastFrameComposer ? 220 : 70, minWidth: isFirstLastFrameComposer ? 220 : 70, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-start', paddingTop: 2, gap: 8 }}>
{/* 层叠附件展示 - 在上传按钮上边 */}
{currentMedia.length > 0 && !(mediaType === 'video' && referenceMode === 'first_last_frame') && (
<div
style={{
width: 58,
flexShrink: 0,
position: 'relative',
}}
onMouseEnter={openMediaStackTray}
onMouseLeave={closeMediaStackTray}
>
<div style={{ position: 'relative', width: 58, height: 66 }}>
{currentMedia.slice(0, 3).map((media, idx) => {
const reversedIdx = Math.min(currentMedia.slice(0, 3).length, 3) - 1 - idx;
const offsetX = mediaStackHovered ? idx * 8 : idx * 4;
const offsetY = mediaStackHovered ? idx * 6 : idx * 3;
return (
<div
key={`stack-${idx}`}
style={{
position: 'absolute',
left: offsetX,
top: offsetY,
zIndex: idx + 1,
transition: 'all 0.28s cubic-bezier(0.4, 0, 0.2, 1)',
transform: `scale(${1 - reversedIdx * 0.06})`,
opacity: mediaStackHovered ? 1 : (1 - reversedIdx * 0.15),
}}
>
<div style={{ position: 'relative', width: 50, height: 58 }}>
{media.type === 'image' ? (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
alt={media.name}
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('image');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 50, height: 58, objectFit: 'cover', borderRadius: 12, cursor: 'pointer', border: '2px solid rgba(255,255,255,0.98)', boxShadow: '0 6px 16px rgba(31,41,55,0.2)' }}
/>
) : (
<video
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
muted
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('video');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 50, height: 58, objectFit: 'cover', borderRadius: 12, cursor: 'pointer', border: '2px solid rgba(255,255,255,0.98)', boxShadow: '0 6px 16px rgba(31,41,55,0.2)' }}
/>
)}
</div>
</div>
);
})}
{currentMedia.length > 3 && (
<div style={{ position: 'absolute', right: 0, bottom: 2, zIndex: 10, background: '#8b5cf6', color: '#fff', fontSize: 9, fontWeight: 700, padding: '2px 6px', borderRadius: 999, boxShadow: '0 3px 8px rgba(31,41,55,0.25)' }}>
+{currentMedia.length - 3}
</div>
)}
</div>
{mediaStackHovered && currentMedia.length > 0 && (
<div
onMouseEnter={openMediaStackTray}
onMouseLeave={closeMediaStackTray}
style={{
position: 'absolute',
left: -10,
bottom: 70,
width: 280,
padding: '12px 12px 12px',
borderRadius: 16,
background: 'rgba(255,255,255,0.98)',
border: '1px solid #E7EAF0',
boxShadow: '0 20px 48px rgba(31,41,55,0.18)',
backdropFilter: 'blur(16px)',
zIndex: 60,
pointerEvents: 'auto',
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
<span style={{ fontSize: 12, color: '#344054', fontWeight: 700 }}></span>
<span style={{ fontSize: 11, color: '#98A2B3', fontWeight: 600 }}>{currentMedia.length}/{maxImageCount + maxVideoCount}</span>
</div>
<div style={{ display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 }}>
{currentMedia.map((media, idx) => (
<div key={`tray-${idx}`} style={{ position: 'relative', flex: '0 0 auto', width: 54 }}>
{media.type === 'image' ? (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
alt={media.name}
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('image');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 54, height: 58, objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid #E7EAF0' }}
/>
) : (
<video
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
muted
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('video');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 54, height: 58, objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid #E7EAF0' }}
/>
)}
<button
onClick={() => handleRemoveMedia(idx)}
style={{
position: 'absolute',
top: 4,
right: 4,
width: 20,
height: 20,
border: 'none',
background: 'rgba(0,0,0,0.5)',
borderRadius: 50,
cursor: 'pointer',
color: '#fff',
fontSize: 10,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backdropFilter: 'blur(4px)',
zIndex: 10,
}}
>
<DeleteOutlined style={{ fontSize: 10 }} />
</button>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* 首尾帧模式 */} {/* 首尾帧模式 */}
{mediaType === 'video' && referenceMode === 'first_last_frame' ? ( {mediaType === 'video' && referenceMode === 'first_last_frame' ? (
@@ -2345,63 +2202,216 @@ const AIChatPage: React.FC = () => {
</div>) : ( </div>) : (
<> <>
{/* 上传按钮 - 非首尾帧模式且还有上传空间时始终显示 */} {/* 上传区域 - 图片堆叠展示,右下角圆形+按钮 */}
{!(mediaType === 'video' && referenceMode === 'first_last_frame') && ( {!(mediaType === 'video' && referenceMode === 'first_last_frame') && (
<div style={{ width: '100%', display: 'flex', justifyContent: 'center', minHeight: 84 }}> <div
<Upload style={{
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'} position: 'relative',
showUploadList={false} width: 64,
beforeUpload={handleUpload} height: 72,
> marginTop: '-10',
<Tooltip title={mediaType === 'image' }}
? `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount}` onMouseEnter={openMediaStackTray}
: `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount},视频${currentMedia.filter(m => m.type === 'video').length}/${maxVideoCount}` onMouseLeave={closeMediaStackTray}
}> >
<div {/* 没有上传时的卡片样式 */}
style={{ {currentMedia.length === 0 && (
width: 54, <Upload
height: 74, accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
borderRadius: 7, showUploadList={false}
border: '1px solid rgba(231, 234, 240, 0.95)', beforeUpload={handleUpload}
display: 'flex', >
alignItems: 'center', <Tooltip title={mediaType === 'image'
justifyContent: 'center', ? `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount}`
cursor: 'pointer', : `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount},视频${currentMedia.filter(m => m.type === 'video').length}/${maxVideoCount}`
transition: 'all 0.25s ease', }>
background: '#ffffff', <div
flexDirection: 'column', style={{
gap: 5, width: 54,
transform: 'rotate(-7deg)', height: 74,
boxShadow: '0 9px 20px rgba(47, 52, 64, 0.10), inset 0 1px 0 rgba(255,255,255,0.95)', borderRadius: 7,
}} border: '1px solid rgba(231, 234, 240, 0.95)',
onMouseEnter={(e) => { display: 'flex',
e.currentTarget.style.borderColor = '#D7DDE7'; alignItems: 'center',
e.currentTarget.style.background = 'linear-gradient(180deg, #ffffff 0%, #F7F8FA 100%)'; justifyContent: 'center',
e.currentTarget.style.transform = 'rotate(0deg) translateY(-2px)'; cursor: 'pointer',
e.currentTarget.style.boxShadow = '0 14px 28px rgba(47, 52, 64, 0.15), inset 0 1px 0 rgba(255,255,255,0.98)'; transition: 'all 0.25s ease',
}} background: '#ffffff',
onMouseLeave={(e) => { flexDirection: 'column',
e.currentTarget.style.borderColor = 'rgba(231, 234, 240, 0.95)'; gap: 5,
e.currentTarget.style.background = 'linear-gradient(180deg, #ffffff 0%, #FAFBFC 100%)'; transform: 'rotate(-7deg)',
e.currentTarget.style.transform = 'rotate(-7deg)'; boxShadow: '0 9px 20px rgba(47, 52, 64, 0.10), inset 0 1px 0 rgba(255,255,255,0.95)',
e.currentTarget.style.boxShadow = '0 9px 20px rgba(47, 52, 64, 0.10), inset 0 1px 0 rgba(255,255,255,0.95)'; }}
}} onMouseEnter={(e) => {
> e.currentTarget.style.borderColor = '#D7DDE7';
{uploading ? ( e.currentTarget.style.background = 'linear-gradient(180deg, #ffffff 0%, #F7F8FA 100%)';
<LoadingOutlined style={{ fontSize: 17, color: '#667085' }} /> e.currentTarget.style.transform = 'rotate(0deg) translateY(-2px)';
) : ( e.currentTarget.style.boxShadow = '0 14px 28px rgba(47, 52, 64, 0.15), inset 0 1px 0 rgba(255,255,255,0.98)';
<> }}
<PlusOutlined style={{ fontSize: 18, color: '#667085', lineHeight: 1 }} /> onMouseLeave={(e) => {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 14px)', columnGap: 2, justifyContent: 'center', color: '#344054', fontSize: 12, fontWeight: 700, lineHeight: 1.05, letterSpacing: 0 }}> e.currentTarget.style.borderColor = 'rgba(231, 234, 240, 0.95)';
<span style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}><span></span><span></span></span> e.currentTarget.style.background = 'linear-gradient(180deg, #ffffff 0%, #FAFBFC 100%)';
<span style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}><span></span><span></span></span> e.currentTarget.style.transform = 'rotate(-7deg)';
e.currentTarget.style.boxShadow = '0 9px 20px rgba(47, 52, 64, 0.10), inset 0 1px 0 rgba(255,255,255,0.95)';
}}
>
{uploading ? (
<LoadingOutlined style={{ fontSize: 17, color: '#667085' }} />
) : (
<>
<PlusOutlined style={{ fontSize: 18, color: '#667085', lineHeight: 1 }} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 14px)', columnGap: 2, justifyContent: 'center', color: '#344054', fontSize: 12, fontWeight: 700, lineHeight: 1.05, letterSpacing: 0 }}>
<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>
</>
)}
</div>
</Tooltip>
</Upload>
)}
{/* 层叠附件展示 - 鼠标移入向右排列展开 */}
{currentMedia.length > 0 && (
<>
{currentMedia.map((media, idx) => {
const reversedIdx = Math.min(currentMedia.length, 3) - 1 - idx;
const offsetX = mediaStackHovered ? idx * 58 : (idx < 3 ? idx * 6 : 18);
const isVisible = !mediaStackHovered ? idx < 3 : true;
return (
<div
key={`stack-${idx}`}
style={{
position: 'absolute',
left: offsetX,
bottom: 0,
zIndex: idx + 1,
transition: 'all 0.28s cubic-bezier(0.4, 0, 0.2, 1)',
transform: mediaStackHovered ? `scale(1) rotate(0deg)` : `scale(${idx < 3 ? (1 - reversedIdx * 0.08) : 0}) rotate(${idx < 3 ? (-3 + idx * 1.5) : 0}deg)`,
opacity: isVisible ? (mediaStackHovered ? 1 : (1 - reversedIdx * 0.15)) : 0,
pointerEvents: isVisible ? 'auto' : 'none',
}}
>
<div style={{ position: 'relative', width: 52, height: 60 }}>
{media.type === 'image' ? (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
alt={media.name}
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('image');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 52, height: 60, objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid rgba(255,255,255,0.98)', boxShadow: '0 4px 12px rgba(31,41,55,0.15)' }}
/>
) : media.type === 'video' ? (
<video
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
muted
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('video');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 52, height: 60, objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid rgba(255,255,255,0.98)', boxShadow: '0 4px 12px rgba(31,41,55,0.15)' }}
/>
) : (
<div
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('audio');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 52, height: 60, objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid rgba(255,255,255,0.98)', boxShadow: '0 4px 12px rgba(31,41,55,0.15)', background: 'linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<AudioOutlined style={{ fontSize: 20, color: '#fff' }} />
</div>
)}
{/* 右上角删除按钮 */}
<button
onClick={() => handleRemoveMedia(idx)}
style={{
position: 'absolute',
top: -6,
right: -6,
width: 20,
height: 20,
borderRadius: 50,
background: '#1f2937',
border: 'none',
color: '#fff',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 6px rgba(0,0,0,0.2)',
zIndex: 10,
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#dc2626';
e.currentTarget.style.transform = 'scale(1.1)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1f2937';
e.currentTarget.style.transform = 'scale(1)';
}}
>
<DeleteOutlined style={{ fontSize: 10 }} />
</button>
</div> </div>
{/* <span style={{ fontSize: 9, color: '#98a2b3', fontWeight: 600, lineHeight: 1 }}>{currentMedia.length}/{maxImageCount + maxVideoCount}</span> */} </div>
</> );
)} })}
</div> </>
</Tooltip> )}
</Upload>
{/* 右下角圆形+上传按钮 */}
{currentMedia.length > 0 && (
<Upload
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
showUploadList={false}
beforeUpload={handleUpload}
>
<Tooltip title={mediaType === 'image'
? `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount}`
: `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount},视频${currentMedia.filter(m => m.type === 'video').length}/${maxVideoCount}`
}>
<div
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: 28,
height: 28,
borderRadius: 50,
background: '#ffffff',
border: '1px solid rgba(231, 234, 240, 0.95)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
transition: 'all 0.2s ease',
boxShadow: '0 2px 8px rgba(47, 52, 64, 0.08)',
zIndex: 100,
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#8b5cf6';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(139, 92, 246, 0.2)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'rgba(231, 234, 240, 0.95)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(47, 52, 64, 0.08)';
}}
>
<PlusOutlined style={{ fontSize: 14, color: '#8b5cf6', lineHeight: 1 }} />
</div>
</Tooltip>
</Upload>
)}
</div> </div>
)} )}
</> </>
@@ -2521,36 +2531,144 @@ const AIChatPage: React.FC = () => {
创作类型: 创作类型:
</span> */} </span> */}
{/* 文件类型选择器 */} {/* 文件类型选择器 */}
<Select <div style={{ position: 'relative', display: 'inline-block' }}>
className="ai-composer-select" <button
value={mediaType} onClick={() => {
onChange={(val) => setMediaType(val)} setShowEngineModal(false);
style={{ setShowImageSettingsModal(false);
minWidth: 118, setShowVideoSettingsModal(false);
padding: '6px 14px', setReferenceModeDropdownVisible(false);
height: 32, setShowMediaTypeModal(prev => !prev);
outline: 'none', }}
border: '1px solid #E7EAF0', className="image-settings-trigger"
backgroundColor: '#ffffff', style={{
borderRadius: 10, minWidth: 118,
boxShadow: '0 1px 2px rgba(47, 52, 64, 0.025)', padding: '5px 12px',
}} height: 32,
size="middle" borderRadius: 10,
> border: '1px solid #E7EAF0',
<Option value="video"> backgroundColor: '#ffffff',
<VideoCameraOutlined style={{ marginRight: 6, fontSize: 14, color: '#8b5cf6' }} /> cursor: 'pointer',
<span style={{ fontSize: 13, fontWeight: 650, color: '#475467' }}></span> display: 'inline-flex',
</Option> alignItems: 'center',
<Option value="image"> gap: 6,
<PictureOutlined style={{ marginRight: 6, fontSize: 14, color: '#8b5cf6' }} /> transition: 'all 0.2s ease',
<span style={{ fontSize: 13, fontWeight: 650, color: '#475467' }}></span> boxShadow: '0 1px 2px rgba(47, 52, 64, 0.025)',
</Option> }}
</Select> onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#FFFFFF';
e.currentTarget.style.borderColor = '#D7DDE7';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(47, 52, 64, 0.06)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = '#ffffff';
e.currentTarget.style.borderColor = '#E7EAF0';
e.currentTarget.style.boxShadow = '0 1px 2px rgba(47, 52, 64, 0.03)';
}}
>
{mediaType === 'video' ? (
<VideoCameraOutlined style={{ fontSize: 14, color: '#8b5cf6' }} />
) : (
<PictureOutlined style={{ fontSize: 14, color: '#8b5cf6' }} />
)}
<Text style={{
fontSize: 13,
fontWeight: 500,
color: '#2f3440',
}}>
{mediaType === 'video' ? '视频生成' : '图片生成'}
</Text>
<CaretDownOutlined style={{ fontSize: 10, color: '#8b5cf6', marginLeft: 'auto' }} />
</button>
{showMediaTypeModal && (
<div
className="image-settings-popover"
style={{
position: 'absolute',
bottom: 'calc(100% + 8px)',
left: 0,
width: 300,
backgroundColor: '#ffffff',
backdropFilter: 'blur(16px)',
borderRadius: 16,
boxShadow: '0 20px 48px rgba(47, 52, 64, 0.12)',
padding: 16,
border: '1px solid #E7EAF0',
zIndex: 9999,
}}
onClick={(e) => e.stopPropagation()}
>
<div style={{ marginBottom: 8 }}>
<Text style={{
display: 'block',
marginBottom: 8,
fontSize: 12,
fontWeight: 600,
color: '#475467',
}}>
</Text>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}>
{[
{ value: 'video', label: '视频生成', icon: VideoCameraOutlined },
{ value: 'image', label: '图片生成', icon: PictureOutlined },
].map((item) => (
<button
key={item.value}
onClick={() => {
setMediaType(item.value);
setShowMediaTypeModal(false);
}}
style={{
flex: 1,
minHeight: 48,
borderRadius: 10,
border: mediaType === item.value
? '1px solid #8b5cf6'
: '1px solid #E7EAF0',
backgroundColor: mediaType === item.value
? '#f5f3ff'
: '#ffffff',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
padding: '8px 12px',
transition: 'all 0.2s',
textAlign: 'left',
gap: 8,
}}
>
<item.icon style={{ fontSize: 14, color: mediaType === item.value ? '#8b5cf6' : '#98a2b3' }} />
<span style={{
fontSize: 14,
fontWeight: mediaType === item.value ? 600 : 500,
color: mediaType === item.value ? '#8b5cf6' : '#475467',
}}>
{item.label}
</span>
</button>
))}
</div>
</div>
</div>
)}
</div>
{/* 引擎选择器 */} {/* 引擎选择器 */}
<div style={{ position: 'relative', display: 'inline-block' }}> <div style={{ position: 'relative', display: 'inline-block' }}>
<button <button
onClick={() => setShowEngineModal(prev => !prev)} onClick={() => {
setShowMediaTypeModal(false);
setShowImageSettingsModal(false);
setShowVideoSettingsModal(false);
setReferenceModeDropdownVisible(false);
setShowEngineModal(prev => !prev);
}}
className="image-settings-trigger" className="image-settings-trigger"
style={{ style={{
minWidth: 174, minWidth: 174,
@@ -2692,7 +2810,13 @@ const AIChatPage: React.FC = () => {
{mediaType === 'video' && (supportsFirstLastFrame || supportsUniversalReference) && ( {mediaType === 'video' && (supportsFirstLastFrame || supportsUniversalReference) && (
<div style={{ position: 'relative', display: 'inline-block' }}> <div style={{ position: 'relative', display: 'inline-block' }}>
<button <button
onClick={() => setReferenceModeDropdownVisible(!referenceModeDropdownVisible)} onClick={() => {
setShowMediaTypeModal(false);
setShowEngineModal(false);
setShowImageSettingsModal(false);
setShowVideoSettingsModal(false);
setReferenceModeDropdownVisible(!referenceModeDropdownVisible);
}}
className="image-settings-trigger" className="image-settings-trigger"
style={{ style={{
minWidth: 118, minWidth: 118,
@@ -2731,127 +2855,117 @@ const AIChatPage: React.FC = () => {
</button> </button>
{referenceModeDropdownVisible && ( {referenceModeDropdownVisible && (
<> <div
<div className="image-settings-popover"
style={{ style={{
position: 'fixed', position: 'absolute',
top: 0, bottom: 'calc(100% + 8px)',
left: 0, left: 0,
right: 0, width: 300,
bottom: 0, backgroundColor: '#ffffff',
zIndex: 9998, backdropFilter: 'blur(16px)',
}} borderRadius: 16,
onClick={() => setReferenceModeDropdownVisible(false)} boxShadow: '0 20px 48px rgba(47, 52, 64, 0.12)',
/> padding: 16,
<div border: '1px solid #E7EAF0',
className="image-settings-popover" zIndex: 9999,
style={{ }}
position: 'absolute', onClick={(e) => e.stopPropagation()}
bottom: 'calc(100% + 8px)', >
left: 0, <div style={{ marginBottom: 8 }}>
minWidth: 160, <Text style={{
backgroundColor: '#ffffff', display: 'block',
backdropFilter: 'blur(16px)', marginBottom: 8,
borderRadius: 16, fontSize: 12,
boxShadow: '0 20px 48px rgba(47, 52, 64, 0.12)', fontWeight: 600,
padding: 8, color: '#475467',
border: '1px solid #E7EAF0', }}>
zIndex: 9999,
}} </Text>
onClick={(e) => e.stopPropagation()} <div style={{
> display: 'flex',
<button flexDirection: 'column',
onClick={() => handleReferenceModeChange('universal')} gap: 4,
disabled={!supportsUniversalReference} }}>
style={{ <button
width: '100%', onClick={() => {
padding: '10px 14px', handleReferenceModeChange('universal');
borderRadius: 10, setReferenceModeDropdownVisible(false);
border: referenceMode === 'universal' }}
? '1px solid rgba(139, 92, 246, 0.3)' disabled={!supportsUniversalReference}
: '1px solid transparent', style={{
backgroundColor: referenceMode === 'universal' flex: 1,
? 'rgba(139, 92, 246, 0.08)' minHeight: 48,
: 'transparent', borderRadius: 10,
cursor: supportsUniversalReference ? 'pointer' : 'not-allowed', border: referenceMode === 'universal'
display: 'flex', ? '1px solid #8b5cf6'
alignItems: 'center', : '1px solid #E7EAF0',
gap: 8, backgroundColor: referenceMode === 'universal'
transition: 'all 0.2s', ? '#f5f3ff'
textAlign: 'left', : '#ffffff',
opacity: supportsUniversalReference ? 1 : 0.4, cursor: supportsUniversalReference ? 'pointer' : 'not-allowed',
}} display: 'flex',
onMouseEnter={(e) => { alignItems: 'center',
if (supportsUniversalReference && referenceMode !== 'universal') { padding: '8px 12px',
e.currentTarget.style.backgroundColor = 'rgba(139, 92, 246, 0.04)'; transition: 'all 0.2s',
} textAlign: 'left',
}} gap: 8,
onMouseLeave={(e) => { opacity: supportsUniversalReference ? 1 : 0.4,
if (referenceMode !== 'universal') { }}
e.currentTarget.style.backgroundColor = 'transparent'; >
} <PictureOutlined style={{ fontSize: 14, color: referenceMode === 'universal' ? '#8b5cf6' : '#98a2b3' }} />
}} <span style={{
> fontSize: 14,
<PictureOutlined style={{ fontSize: 14, color: '#8b5cf6' }} /> fontWeight: referenceMode === 'universal' ? 600 : 500,
<span style={{ color: referenceMode === 'universal' ? '#8b5cf6' : '#475467',
fontSize: 13, }}>
fontWeight: referenceMode === 'universal' ? 600 : 500,
color: referenceMode === 'universal' ? '#8b5cf6' : '#475467', </span>
flex: 1, {!supportsUniversalReference && (
}}> <span style={{ fontSize: 10, color: '#98a2b3', marginLeft: 'auto' }}></span>
)}
</span> </button>
{!supportsUniversalReference && ( <button
<span style={{ fontSize: 10, color: '#98a2b3' }}></span> onClick={() => {
)} handleReferenceModeChange('first_last_frame');
</button> setReferenceModeDropdownVisible(false);
<button }}
onClick={() => handleReferenceModeChange('first_last_frame')} disabled={!supportsFirstLastFrame}
disabled={!supportsFirstLastFrame} style={{
style={{ flex: 1,
width: '100%', minHeight: 48,
padding: '10px 14px', borderRadius: 10,
borderRadius: 10, border: referenceMode === 'first_last_frame'
border: referenceMode === 'first_last_frame' ? '1px solid #8b5cf6'
? '1px solid rgba(139, 92, 246, 0.3)' : '1px solid #E7EAF0',
: '1px solid transparent', backgroundColor: referenceMode === 'first_last_frame'
backgroundColor: referenceMode === 'first_last_frame' ? '#f5f3ff'
? 'rgba(139, 92, 246, 0.08)' : '#ffffff',
: 'transparent', cursor: supportsFirstLastFrame ? 'pointer' : 'not-allowed',
cursor: supportsFirstLastFrame ? 'pointer' : 'not-allowed', display: 'flex',
display: 'flex', alignItems: 'center',
alignItems: 'center', padding: '8px 12px',
gap: 8, transition: 'all 0.2s',
transition: 'all 0.2s', textAlign: 'left',
textAlign: 'left', gap: 8,
opacity: supportsFirstLastFrame ? 1 : 0.4, opacity: supportsFirstLastFrame ? 1 : 0.4,
}} }}
onMouseEnter={(e) => { >
if (supportsFirstLastFrame && referenceMode !== 'first_last_frame') { <SwapOutlined style={{ fontSize: 14, color: referenceMode === 'first_last_frame' ? '#8b5cf6' : '#98a2b3' }} />
e.currentTarget.style.backgroundColor = 'rgba(139, 92, 246, 0.04)'; <span style={{
} fontSize: 14,
}} fontWeight: referenceMode === 'first_last_frame' ? 600 : 500,
onMouseLeave={(e) => { color: referenceMode === 'first_last_frame' ? '#8b5cf6' : '#475467',
if (referenceMode !== 'first_last_frame') { }}>
e.currentTarget.style.backgroundColor = 'transparent';
} </span>
}} {!supportsFirstLastFrame && (
> <span style={{ fontSize: 10, color: '#98a2b3', marginLeft: 'auto' }}></span>
<SwapOutlined style={{ fontSize: 14, color: '#8b5cf6' }} /> )}
<span style={{ </button>
fontSize: 13, </div>
fontWeight: referenceMode === 'first_last_frame' ? 600 : 500,
color: referenceMode === 'first_last_frame' ? '#8b5cf6' : '#475467',
flex: 1,
}}>
</span>
{!supportsFirstLastFrame && (
<span style={{ fontSize: 10, color: '#98a2b3' }}></span>
)}
</button>
</div> </div>
</> </div>
)} )}
</div> </div>
)} )}
@@ -2860,7 +2974,13 @@ const AIChatPage: React.FC = () => {
{mediaType === 'image' && ( {mediaType === 'image' && (
<div style={{ position: 'relative', display: 'inline-block' }}> <div style={{ position: 'relative', display: 'inline-block' }}>
<button <button
onClick={() => setShowImageSettingsModal(!showImageSettingsModal)} onClick={() => {
setShowMediaTypeModal(false);
setShowEngineModal(false);
setShowVideoSettingsModal(false);
setReferenceModeDropdownVisible(false);
setShowImageSettingsModal(!showImageSettingsModal);
}}
className="image-settings-trigger" className="image-settings-trigger"
style={{ style={{
minWidth: 156, minWidth: 156,
@@ -3164,7 +3284,13 @@ const AIChatPage: React.FC = () => {
{mediaType === 'video' && ( {mediaType === 'video' && (
<div style={{ position: 'relative', display: 'inline-block' }}> <div style={{ position: 'relative', display: 'inline-block' }}>
<button <button
onClick={() => setShowVideoSettingsModal(!showVideoSettingsModal)} onClick={() => {
setShowMediaTypeModal(false);
setShowEngineModal(false);
setShowImageSettingsModal(false);
setReferenceModeDropdownVisible(false);
setShowVideoSettingsModal(!showVideoSettingsModal);
}}
className="image-settings-trigger" className="image-settings-trigger"
style={{ style={{
minWidth: 156, minWidth: 156,