From 20bd5e84c5f29ad3f97cb7bcf41c5a3ceb223ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E4=BD=B3=E8=89=BA?= Date: Mon, 1 Jun 2026 09:19:31 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=9F=E6=88=90=E5=8E=86=E5=8F=B2=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-app/src/App.tsx | 3 + video-gen-app/src/api/index.ts | 26 + video-gen-app/src/api/mock.ts | 12 + .../src/components/Layout/AppLayout.tsx | 13 + video-gen-app/src/pages/GenerateConver.tsx | 1729 ++++++++++++++++- video-gen-app/src/pages/GeneratedRecord.tsx | 736 +++++++ video-gen-app/src/types/index.ts | 9 +- 7 files changed, 2426 insertions(+), 102 deletions(-) create mode 100644 video-gen-app/src/pages/GeneratedRecord.tsx diff --git a/video-gen-app/src/App.tsx b/video-gen-app/src/App.tsx index 57372339..7eee9ca5 100644 --- a/video-gen-app/src/App.tsx +++ b/video-gen-app/src/App.tsx @@ -11,6 +11,7 @@ import CreditsPage from './pages/CreditsPage'; import GenerateConver from './pages/GenerateConver'; import InitialReplication from './pages/InitialReplication'; import RemoveLens from './pages/RemoveLens'; +import GeneratedRecord from './pages/GeneratedRecord'; @@ -91,6 +92,8 @@ const App = () => { } /> } /> } /> + } /> + diff --git a/video-gen-app/src/api/index.ts b/video-gen-app/src/api/index.ts index 3c833cf1..eaab10db 100644 --- a/video-gen-app/src/api/index.ts +++ b/video-gen-app/src/api/index.ts @@ -280,6 +280,32 @@ export async function getRechargePackages(): Promise { export async function getCreditRatios(): Promise { return api.get('/credits/ratios'); } +// 引擎配置 +export async function getEngine(): Promise { + return api.get('/generation-ai/engines'); +} + +// ── Generation AI Tasks ──────────────────────────────────── + + + +export async function createGenerationTask(params: any): Promise { + return api.post('/generation-ai/tasks', params); +} + +export async function getgen_list(Pagebreak: any): Promise { + return api.get('/generation-ai/tasks?page='+Pagebreak.page+'&page_size='+Pagebreak.pageSize); +} + +export async function gethistory(Pagebreak: any): Promise { + return api.get('/generation-ai/history'+Pagebreak); +} + +export async function gethistoryItems(Pagebreak: any): Promise { + return api.get('/generation-ai/history/'+Pagebreak); +} + + diff --git a/video-gen-app/src/api/mock.ts b/video-gen-app/src/api/mock.ts index 88b8669d..b778310a 100644 --- a/video-gen-app/src/api/mock.ts +++ b/video-gen-app/src/api/mock.ts @@ -364,3 +364,15 @@ export async function mockGetAdminNotifications(): Promise await delay(300); return [...MOCK_ADMIN_NOTIFICATIONS]; } + +// ── Generation AI Tasks Mock ─────────────────────────────── + +export async function mockCreateGenerationTask(params: any): Promise { + await delay(1000); + return { + task_id: `task-${Date.now()}`, + status: 'pending', + message: '任务已创建', + ...params, + }; +} diff --git a/video-gen-app/src/components/Layout/AppLayout.tsx b/video-gen-app/src/components/Layout/AppLayout.tsx index 6e596a0d..d83cd41b 100644 --- a/video-gen-app/src/components/Layout/AppLayout.tsx +++ b/video-gen-app/src/components/Layout/AppLayout.tsx @@ -8,6 +8,8 @@ import { UserOutlined, ThunderboltOutlined, HomeOutlined, + DashboardOutlined, + CodeOutlined, LockOutlined, PlusCircleOutlined, LeftOutlined, @@ -43,6 +45,8 @@ interface MenuConfig { const iconMap: Record = { HomeOutlined: , + DashboardOutlined:, + CodeOutlined:, PlayCircleOutlined: , WalletOutlined: , SettingOutlined: , @@ -86,6 +90,15 @@ const AppLayout: React.FC = () => { const [qrCodeModalOpen, setQrCodeModalOpen] = useState(false); const [currentPaymentInfo, setCurrentPaymentInfo] = useState<{ price: number; credits: number; qrCode: string } | null>(null); + // 监听预览弹窗状态,关闭浮动按钮 + useEffect(() => { + const handleModalOpen = () => { + setToggleHover(false); + }; + window.addEventListener('previewOpen', handleModalOpen); + return () => window.removeEventListener('previewOpen', handleModalOpen); + }, []); + useEffect(() => { getSiteInfo().then(info => { setSiteName(info.siteName || 'VideoGen.AI'); diff --git a/video-gen-app/src/pages/GenerateConver.tsx b/video-gen-app/src/pages/GenerateConver.tsx index 2f339269..f2342c5a 100644 --- a/video-gen-app/src/pages/GenerateConver.tsx +++ b/video-gen-app/src/pages/GenerateConver.tsx @@ -1,4 +1,6 @@ -import React, { useState } from 'react'; + +import React, { useState, useRef, useEffect } from 'react'; + import { Layout, Button, @@ -8,143 +10,1125 @@ import { message, Space, Typography, + Tooltip, + Popconfirm, + Modal, } from 'antd'; + +import { getParameters, createGenerationTask, getgen_list,getEngine,uploadImage, + uploadVideo, } from '../api'; + import { PlusOutlined, MenuUnfoldOutlined, MenuFoldOutlined, SendOutlined, - UploadOutlined, + DeleteOutlined, + RobotOutlined, + LoadingOutlined, + PictureOutlined, + VideoCameraOutlined, + CaretDownOutlined, + SwapOutlined, + WarningOutlined, } from '@ant-design/icons'; const { Header, Sider, Content } = Layout; +// 解构Input组件 const { TextArea } = Input; +// 解构Select组件 const { Option } = Select; +// 解构Typography组件 const { Text } = Typography; -const AIChatPage: React.FC = () => { - // 侧边栏收起/展开状态 - const [collapsed, setCollapsed] = useState(false); - // 输入框内容 - const [inputValue, setInputValue] = useState(''); - // 第一个选择器:文件类型(图片/视频) - const [mediaType, setMediaType] = useState('image'); - // 第二个选择器:1/2/3 - const [countType, setCountType] = useState('1'); - // 模拟“新对话”点击 - const handleNewChat = () => { - message.info('开启新对话'); - setInputValue(''); +interface MediaReference { + name: string; + type: 'image' | 'video'; + url: string; +} + +interface Message { + id: string; + original_prompt: string; + gen_type: string; + idempotency_key: string; + media_references?: MediaReference[]; + image_size?: string; + image_proportion?: string; + image_px?: string; + duration?: number; + ratio?: string; + resolution?: string; + timestamp?: string; + engine_id:string; +} + + +interface Conversation { + id: string; + title: string; + lastMessage: string; + timestamp: string; + messages: any; +} + + +const mockUpload = (file: File): Promise<{ url: string }> => { + return new Promise((resolve) => { + setTimeout(() => { + // 使用URL.createObjectURL创建本地文件预览URL + const url = URL.createObjectURL(file); + resolve({ url }); + }, 500); + }); +}; + + + + +const AIChatPage: React.FC = () => { + // ==================== 状态定义 ==================== + + const [collapsed, setCollapsed] = useState(false); + + const [currentConversationId, setCurrentConversationId] = useState('1'); + + const [conversations, setConversations] = useState([ + ]); + + + + const [inputValue, setInputValue] = useState(''); + + const [mediaType, setMediaType] = useState('image'); + + const [countType, setCountType] = useState('请选择'); + + const [uploading, setUploading] = useState(false); + + const [loading, setLoading] = useState(false); + + const [currentMedia, setCurrentMedia] = useState<{ name: string; type: 'image' | 'video'; url: string }[]>([]); + + const [previewVisible, setPreviewVisible] = useState(false); + + const [previewUrl, setPreviewUrl] = useState(''); + + const [previewType, setPreviewType] = useState<'image' | 'video'>('image'); + + // 附件详情悬浮窗状态 + const [attachmentPopupVisible, setAttachmentPopupVisible] = useState(false); + const [attachmentPopupMessageId, setAttachmentPopupMessageId] = useState(null); + const [attachmentPopupPosition, setAttachmentPopupPosition] = useState({ x: 0, y: 0 }); + + // 图片设置相关状态 + const [selectedRatio, setSelectedRatio] = useState('1:1'); + const [selectedResolution, setSelectedResolution] = useState('2K'); + const [width, setWidth] = useState(2048); + const [height, setHeight] = useState(2048); + const [showImageSettingsModal, setShowImageSettingsModal] = useState(false); + const [ratioOptions, setRatioOptions] = useState([]); + const [resolutionOptions, setResolutionOptions] = useState([]); + const [widthandheight, setWidthandHeight] = useState([]); + const [blindex, setBlindex] = useState(0); + const [fblindex, setFBlindex] = useState(0); + + // 视频设置相关状态 + const [videoDuration, setVideoDuration] = useState(5); + const [videoAspectRatio, setVideoAspectRatio] = useState('16:9'); + const [videoResolution, setVideoResolution] = useState('720p'); + const [expandedEngine, setExpandedEngine] = useState(null); + const [engineOptions, setEngineOptions] = useState<{ + ratios: string[]; + resolutions: string[]; + durations: number[]; + }>({ + ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'], + resolutions: ['480p', '720p', '1080p'], + durations: [5, 8, 10, 12, 15], + }); + + const [enginesele, setEnginesele] = useState([]); + + const messagesEndRef = useRef(null); + const scrollContainerRef = useRef(null); + const pollingRef = useRef(null); + const isFirstLoadRef = useRef(true); + + const currentConversation = conversations.find((c) => c.id === currentConversationId); + + + const [gen_list, setGen_list] = useState([]); + + const [Pagebreak, setPagebreak] = useState({ + page: 1, + pageSize: 20, + }); + const [Totalnumber, setTotalnumber] = useState(0); + const [isLoadingMore, setIsLoadingMore] = useState(false); + // ==================== 副作用 ==================== + + // 初次打开时自动滚动到底部 + useEffect(() => { + if (gen_list.length > 0 && isFirstLoadRef.current) { + const timer = setTimeout(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + isFirstLoadRef.current = false; + }, 100); + return () => clearTimeout(timer); + } + }, [gen_list.length]); + + useEffect(() => { + const newOptions = mediaType === 'image' ? enginesele.image : enginesele.video; + if (newOptions && newOptions.length > 0) { + // 如果当前 countType 不在新选项中,重置为第一个选项 + const isValidOption = newOptions.some((item: any) => item.id === countType); + if (!isValidOption) { + setCountType(newOptions[0].id); + } + + } + }, [mediaType, enginesele]); + + // 初始化获取参数 - 只在组件挂载时执行一次 + useEffect(() => { + getEngine() + .then((data:any) => { + setEnginesele(data.engine); + }) + .catch(() => { + }); + getgen_list(Pagebreak).then((data: any) => { + let mess_list = data.items + let total = data.total + setGen_list(mess_list) + setTotalnumber(total) + }) + getParameters() + .then((data) => { + let supportedSizes = (data as any).items?.[0]?.supportedSizes || {}; + let supportedResolutions = []; + let twokwidth = []; + let fourkwidth = []; + + for (let key in supportedSizes["2K"]) { + supportedResolutions.push(key); + twokwidth.push(supportedSizes["2K"][key]); + } + + const newRatioOptions = supportedResolutions.map((res: any, index: number) => ({ + value: res, + label: String(index), + })); + setRatioOptions(newRatioOptions); + + for (let key in supportedSizes["4K"]) { + fourkwidth.push(supportedSizes["4K"][key]); + } + + const newWidthandHeight = [twokwidth, fourkwidth]; + setWidthandHeight(newWidthandHeight); + setResolutionOptions([ + { value: '2K', label: '高清 2K' }, + { value: '4K', label: '超清 4K' }, + ]); + }) + .catch(() => { }); + }, []); + + // 轮询逻辑 - 当有任务在生成时,每5秒查询一次状态 + useEffect(() => { + // 先清理之前的定时器 + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + + // 判断是否有正在生成的任务 + const hasGeneratingTask = gen_list.some((item: any) => item.status === 'generating'); + + if (!hasGeneratingTask) return; + + // 设置5秒轮询 + pollingRef.current = window.setInterval(() => { + getgen_list(Pagebreak).then((data: any) => { + setGen_list(data.items); + setTotalnumber(data.total) + }).catch((error) => { + }); + }, 5000); + + // 清理定时器 + return () => { + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + }; + }, [gen_list, Pagebreak]); + + // 点击空白处关闭图片设置浮层 + useEffect(() => { + if (!showImageSettingsModal) return; + const handleClickOutside = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if ( + !target.closest(".image-settings-popover") && + !target.closest(".image-settings-trigger") + ) { + setShowImageSettingsModal(false); + } + }; + document.addEventListener("click", handleClickOutside); + return () => document.removeEventListener("click", handleClickOutside); + }, [showImageSettingsModal]); + + // 监听 blindex 状态变化 + useEffect(() => { + if ( + widthandheight[fblindex] && + widthandheight[fblindex][blindex] !== undefined + ) { + setWidth(Number(widthandheight[fblindex][blindex].substring(0, 4))); + setHeight(Number(widthandheight[fblindex][blindex].substring(5))); + } + }, [blindex]); + + useEffect(() => { + if ( + widthandheight[fblindex] && + widthandheight[fblindex][blindex] !== undefined + ) { + setWidth(Number(widthandheight[fblindex][blindex].substring(0, 4))); + setHeight(Number(widthandheight[fblindex][blindex].substring(5))); + } + }, [fblindex]); + + // 根据比例计算尺寸 + const calculateSizeFromRatione = (ratio: string) => { + setBlindex(ratio); }; - // 发送消息 - const handleSend = () => { - if (!inputValue.trim()) { - message.warning('请输入内容'); + const calculateSizeFromRatiotwo = (ratio: string) => { + setFBlindex(parseInt(ratio === '2K' ? '0' : '1')); + }; + + // 交换宽高 + const handleSwap = () => { + setWidth(height); + setHeight(width); + }; + + // ==================== 事件处理函数 ==================== + + const handleNewChat = () => { + // 生成唯一ID(使用时间戳) + const newId = Date.now().toString(); + const newConversation: Conversation = { + id: newId, + title: '新对话', + lastMessage: '', + timestamp: new Date().toLocaleString('zh-CN'), + messages: [], + }; + + // 将新对话插入到列表顶部 + setConversations((prev) => [newConversation, ...prev]); + // 切换到新对话 + setCurrentConversationId(newId); + // 清空输入框和已上传媒体 + setInputValue(''); + setCurrentMedia([]); + // 显示提示消息 + message.info('已开启新对话'); + + }; + + + const handleDeleteChat = (conversationId: string) => { + // 过滤掉要删除的对话 + setConversations((prev) => prev.filter((c) => c.id !== conversationId)); + // 如果删除的是当前选中的对话,切换到其他对话 + if (currentConversationId === conversationId) { + setCurrentConversationId( + conversations[0]?.id === conversationId + ? conversations[1]?.id || null + : conversations[0]?.id || null + ); + } + // 显示成功提示 + message.success('对话已删除'); + }; + + + const handleSelectChat = (conversationId: string) => { + setCurrentConversationId(conversationId); + setCurrentMedia([]); + }; + + + const handleSend = async () => { + // 验证:必须有内容或图片或视频 + if (!inputValue.trim() && currentMedia.length === 0) { + message.warning('请输入内容或上传图片/视频'); return; } - console.log('发送消息:', inputValue, mediaType, countType); - message.success('消息已发送'); + + // 创建用户消息对象 + const newMessage: Message = { + id: '', + gen_type: mediaType, + original_prompt: inputValue.trim(), + engine_id:countType, + + idempotency_key: new Date().toLocaleString('zh-CN'), + // 统一的媒体数组,包含 name、type、url + media_references: currentMedia.length > 0 ? [...currentMedia] : undefined, + // 图片参数(仅图片模式时添加) + ...(mediaType === 'image' && { + image_size: selectedResolution, + image_proportion: selectedRatio, + image_px: width + "x" + height, + }), + // 视频参数(仅视频模式时添加) + ...(mediaType === 'video' && { + duration: videoDuration, + ratio: videoAspectRatio, + resolution: videoResolution, + }), + }; + + + createGenerationTask(newMessage).then(() => { + // 创建任务成功后,重置页数为1,获取最新列表 + const newPagebreak = { ...Pagebreak, page: 1 }; + setPagebreak(newPagebreak); + + getgen_list(newPagebreak).then((data: any) => { + // 将data.items的最后一个元素添加到gen_list末尾 + setGen_list((prev: any[]) => [...prev, data.items[data.items.length - 1]]); + setTotalnumber(data.total); + // 发送消息后滚动到底部 + setTimeout(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, 100); + }).catch((error) => { + console.error('获取列表失败:', error); + }); + }).catch((error) => { + console.error('创建任务失败:', error); + setLoading(false); + }); + + // 清空输入框和已上传媒体 setInputValue(''); + setCurrentMedia([]); + // 设置加载状态 + setLoading(true); + setTimeout(() => { + setLoading(false); + }, 3000); + + }; - // 文件上传前校验(仅示意) - const beforeUpload = (file: File) => { - const isImageOrVideo = file.type.startsWith('image/') || file.type.startsWith('video/'); - if (!isImageOrVideo) { - message.error('只能上传图片或视频文件'); + // 加载更多 + const handleLoadMore = async () => { + // 检查是否还有更多数据 + if (gen_list.length >= Totalnumber) { + return; + } + + // 保存当前滚动位置(相对于顶部的偏移) + const scrollContainer = scrollContainerRef.current; + if (!scrollContainer) { + console.warn('滚动容器未找到'); + return; + } + + const scrollTopBefore = scrollContainer.scrollTop; + const scrollHeightBefore = scrollContainer.scrollHeight; + + setIsLoadingMore(true); + + try { + const nextPage = Pagebreak.page + 1; + const result = await getgen_list({ + ...Pagebreak, + page: nextPage, + }); + + // 处理返回数据,兼容数组或对象格式 + const data = Array.isArray(result) ? { items: result, total: result.length } : result; + + + // 更新总数 + if (data.total) { + setTotalnumber(data.total); + } + + // 更新页码 + setPagebreak((prev: any) => ({ ...prev, page: nextPage })); + + // 去重后合并数据(根据 id 去重,新数据添加到前面) + setGen_list((prev: any[]) => { + const existingIds = new Set(prev.map((item: any) => item.id)); + // 只添加不存在的新数据,保持新数据的原有顺序 + const newItems = data.items.filter((item: any) => { + if (!item.id) return false; + if (existingIds.has(item.id)) return false; + existingIds.add(item.id); + return true; + }); + // 新数据在前,旧数据在后 + return [...newItems, ...prev]; + }); + + // 恢复滚动位置(等待DOM更新完成后执行,增加超时时间确保DOM完全更新) + setTimeout(() => { + const scrollHeightAfter = scrollContainer.scrollHeight; + const addedHeight = scrollHeightAfter - scrollHeightBefore; + // 新数据添加到前面,保持当前查看内容的位置不变 + scrollContainer.scrollTop = scrollTopBefore + addedHeight; + }, 200); + } catch (error) { + } finally { + setIsLoadingMore(false); } - return isImageOrVideo; }; + const handleUpload = async (file: File) => { + // 验证文件类型 + const isImage = file.type.startsWith('image/'); + const isVideo = file.type.startsWith('video/'); + + if (!isImage && !isVideo) { + message.error('仅支持图片或视频文件'); + return false; + } + + // 验证文件大小 + const maxMB = isVideo ? 100 : 10; + if (file.size / 1024 / 1024 > maxMB) { + message.error(`${isVideo ? '视频' : '图片'}大小不能超过${maxMB}MB`); + return false; + } + + // 验证图片数量(最多4张) + const imageCount = currentMedia.filter((m) => m.type === 'image').length; + if (isImage && imageCount >= 4) { + message.error('最多上传4张图片'); + return false; + } + + // 验证视频数量(最多1个) + const videoCount = currentMedia.filter((m) => m.type === 'video').length; + if (isVideo && videoCount >= 1) { + message.error('最多上传1个视频'); + return false; + } + + // 设置上传状态 + setUploading(true); + + try { + // 根据文件类型调用相应的上传函数 + const uploadFn = isImage ? uploadImage : uploadVideo; + const res = await uploadFn(file); + // 添加到统一的媒体列表 + setCurrentMedia((prev) => [...prev, { + name: file.name, + type: isImage ? 'image' : 'video', + url: res.url, + }]); + message.success(`${isImage ? '图片' : '视频'}上传成功`); + } catch (error) { + message.error('上传失败'); + } finally { + setUploading(false); + } + + // 返回false阻止Ant Design的自动上传行为 + return false; + }; + + + const handleRemoveMedia = (index: number) => { + setCurrentMedia((prev) => prev.filter((_, i) => i !== index)); + }; + + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + + + const handleClosePreview = () => { + setPreviewVisible(false); + setPreviewUrl(''); + }; + + const handleDownload = () => { + if (!previewUrl) return; + const link = document.createElement('a'); + link.href = previewUrl; + link.download = previewType === 'image' ? 'image.png' : 'video.mp4'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + // ==================== 渲染 ==================== + return ( - - {/* 左侧边栏 */} - -
- - )} -
-
+ type="text" + icon={collapsed ? : } + onClick={() => setCollapsed(!collapsed)} + style={{ marginBottom: 12, width: '100%' }} + /> + + {!collapsed && ( + + )} + + {!collapsed && conversations.length > 0 && ( +
+ {conversations.map((conversation) => ( +
handleSelectChat(conversation.id)} + style={{ + display: 'flex', + alignItems: 'center', + padding: '10px 12px', + marginBottom: 4, + borderRadius: 8, + cursor: 'pointer', + background: currentConversationId === conversation.id ? '#f0f5ff' : 'transparent', + border: currentConversationId === conversation.id ? '1px solid #e0e8ff' : 'none', + transition: 'all 0.2s', + }} + onMouseEnter={(e) => { + e.currentTarget.style.background = currentConversationId === conversation.id ? '#f0f5ff' : '#fafafa'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = currentConversationId === conversation.id ? '#f0f5ff' : 'transparent'; + }} + > +
+

+ {conversation.title} +

+

+ {conversation.lastMessage || '暂无消息'} +

+
+ handleDeleteChat(conversation.id)} + okText="确定" + cancelText="取消" + > +
+ ))} +
+ )} + + + )} {/* 主内容区 */} - + + {/* 头部 - 显示对话标题和模型信息 */}
- 开启创作 +
+ + {'开启创作'} + +
+ {/*
+ 模型: Gemini 3 Pro + 比例: 9:16 + 消耗 15 积分 +
*/}
+ {/* 消息区域 */} - {/* 中间提示语 */} -
-

你好,想创作什么?

-
+ {/* 空状态 - 没有对话或当前对话没有消息时显示 */} + {gen_list.length === 0 && ( +
+
+ +
+

+ 你好,想创作什么? +

+

+ 输入想法、剧本或上传参考,开始你的创作之旅 +

+
+ )} - {/* 底部输入区域 */} + {/* 消息列表 - gen_list 有数据时显示 */} + {gen_list.length > 0 && ( +
+ {/* 加载更多按钮 - 在列表顶部 */} +
+ {gen_list.length >= Totalnumber ? ( + 消息全部加载 + ) : ( + + )} +
+ {/* 遍历消息列表 */} + {gen_list.map((message) => ( +
+
+ {/* 头像 */} +
+ +
+ + {/* 消息内容 */} +
+ + {/* 消息气泡 */} +
+ {/* 文本内容 */} +

+ {message.originalPrompt} + {message.mediaReferences && message.mediaReferences.length > 0 && ( + { + e.stopPropagation(); + const target = e.currentTarget as HTMLElement; + const rect = target.getBoundingClientRect(); + setAttachmentPopupPosition({ + x: rect.left, + y: rect.top - 10 + }); + setAttachmentPopupMessageId(message.id); + setAttachmentPopupVisible(true); + }} + > + 附件详情 + + )} +

+ {/* 根据 status 显示不同内容 */} + {/* 生成中 - 显示加载动画 */} + {message.status === 'generating' && ( +
+
+
+ + + +
+
+
+ )} + {/* 生成失败 - 显示失败提示 */} + {message.status === 'failed' && ( +
+ + 生成失败,请重试 +
+ )} + {/* 已完成 - 显示媒体内容 */} + {message.status === 'completed' && ( +
+
{ + setPreviewUrl(message.genType === 'image' ? message.imageUrl : message.videoUrl); + setPreviewType(message.genType === 'image' ? 'image' : 'video'); + setPreviewVisible(true); + }} + style={{ cursor: 'pointer', overflow: 'hidden', borderRadius: 8, position: 'relative' }} + > + {message.genType === 'image' ? ( + {message.name} { e.currentTarget.style.transform = 'scale(1.05)'; }} + onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }} + /> + ) : ( + <> +
+
+ )} + +
+ {/* 时间戳和参数信息 */} + +
+ {message.createdAt?.replace('T', ' ').split('.')[0]} + {message.genType === 'image' + ? `${message.imageProportion || ''} ${message.imagePx || ''} ${message.imageSize || ''}` + : `${message.duration || ''}s ${message.aspectRatio || ''} ${message.resolution || ''}` + } + {message.engineSnapshot.name} +
+
+ +
+
+ ))} + + + + {/* 消息列表底部标记 - 用于自动滚动 */} +
+
+ )} + + {/* 附件详情悬浮窗 - 包含遮罩层 */} + {attachmentPopupVisible && attachmentPopupMessageId && ( + <> + {/* 遮罩层 - 点击关闭悬浮窗 */} +
{ + setAttachmentPopupVisible(false); + setAttachmentPopupMessageId(null); + }} + /> + {/* 悬浮窗内容 */} +
e.stopPropagation()} + > +
+ 附件列表 + +
+ {gen_list.find((msg: any) => msg.id === attachmentPopupMessageId)?.mediaReferences?.map((ref: any, idx: number) => ( +
{ + const link = document.createElement('a'); + link.href =`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${ref.url}` ; + link.download = ref.name; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }} + style={{ + display: 'flex', + alignItems: 'center', + gap: 8, + padding: '8px 12px', + borderRadius: 6, + cursor: 'pointer', + fontSize: 13, + color: '#333', + transition: 'background 0.2s' + }} + onMouseEnter={(e) => e.currentTarget.style.background = '#f5f5f5'} + onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'} + > + {ref.type === 'image' ? ( + + ) : ( + + )} + {ref.name} +
+ ))} +
+ + )} + + {/* 输入区域 - 始终显示 */}
-
- {/* 文件上传按钮 */} + {/* 已上传媒体预览 */} + {currentMedia.length > 0 && ( +
+ {currentMedia.map((media, idx) => ( +
+ {media.type === 'image' ? ( + {media.name} + ) : ( +
+ ))} +
+ )} + + {/* 输入框区域 */} +
+ {/* 上传按钮 */} -