forked from 77media/video-flow
chatbox 加入 添加分镜视频
This commit is contained in:
parent
fef7e0ed26
commit
8a0474fbf8
@ -27,6 +27,7 @@ import { AudioRecorder } from "./AudioRecorder";
|
||||
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { createMovieProjectV1 } from "@/api/video_flow";
|
||||
import { createScriptEpisodeNew } from "@/api/script_episode";
|
||||
import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service";
|
||||
import { ActionButton } from "../common/ActionButton";
|
||||
import { HighlightEditor } from "../common/HighlightEditor";
|
||||
@ -608,7 +609,7 @@ export function ChatInputBox() {
|
||||
};
|
||||
|
||||
// 调用创建剧集API
|
||||
const episodeResponse = await createMovieProjectV1(episodeData);
|
||||
const episodeResponse = await createScriptEpisodeNew(episodeData);
|
||||
console.log("episodeResponse", episodeResponse);
|
||||
if (episodeResponse.code !== 0) {
|
||||
console.error(`创建剧集失败: ${episodeResponse.message}`);
|
||||
|
||||
@ -1,29 +1,48 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { Image as ImageIcon, Send, Trash2 } from "lucide-react";
|
||||
import { MessageBlock } from "./types";
|
||||
import { useUploadFile } from "@/app/service/domain/service";
|
||||
|
||||
interface InputBarProps {
|
||||
onSend: (blocks: MessageBlock[]) => void;
|
||||
onSend: (blocks: MessageBlock[], videoId?: string) => void;
|
||||
setVideoPreview?: (url: string, id: string) => void;
|
||||
initialVideoUrl?: string;
|
||||
initialVideoId?: string;
|
||||
}
|
||||
|
||||
export function InputBar({ onSend }: InputBarProps) {
|
||||
export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVideoId }: InputBarProps) {
|
||||
const [text, setText] = useState("");
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
const [videoUrl, setVideoUrl] = useState<string | null>(initialVideoUrl || null);
|
||||
const [videoId, setVideoId] = useState<string | null>(initialVideoId || null);
|
||||
|
||||
const { uploadFile } = useUploadFile();
|
||||
|
||||
// 监听初始视频 URL 和 ID 的变化
|
||||
useEffect(() => {
|
||||
if (initialVideoUrl && initialVideoId) {
|
||||
setVideoUrl(initialVideoUrl);
|
||||
setVideoId(initialVideoId);
|
||||
}
|
||||
}, [initialVideoUrl, initialVideoId]);
|
||||
|
||||
const handleSend = () => {
|
||||
const blocks: MessageBlock[] = [];
|
||||
if (text.trim()) blocks.push({ type: "text" as const, text: text.trim() });
|
||||
if (imageUrl) blocks.push({ type: "image" as const, url: imageUrl });
|
||||
if (videoUrl) blocks.push({ type: "video" as const, url: videoUrl });
|
||||
if (!blocks.length) return;
|
||||
|
||||
onSend(blocks);
|
||||
onSend(blocks, videoId || undefined);
|
||||
setText("");
|
||||
setImageUrl(null);
|
||||
if (videoUrl && videoId && setVideoPreview) {
|
||||
setVideoPreview(videoUrl, videoId);
|
||||
}
|
||||
setVideoUrl(null);
|
||||
setVideoId(null);
|
||||
};
|
||||
|
||||
const handleFileUpload = async (file: File) => {
|
||||
@ -32,7 +51,17 @@ export function InputBar({ onSend }: InputBarProps) {
|
||||
const url = await uploadFile(file, (progress) => {
|
||||
setUploadProgress(progress);
|
||||
});
|
||||
// 如果已经有视频,先保存视频状态
|
||||
const prevVideoUrl = videoUrl;
|
||||
const prevVideoId = videoId;
|
||||
|
||||
setImageUrl(url);
|
||||
|
||||
// 恢复视频状态
|
||||
if (prevVideoUrl && prevVideoId) {
|
||||
setVideoUrl(prevVideoUrl);
|
||||
setVideoId(prevVideoId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("上传失败:", error);
|
||||
// 可以添加错误提示
|
||||
@ -65,9 +94,10 @@ export function InputBar({ onSend }: InputBarProps) {
|
||||
|
||||
return (
|
||||
<div data-alt="input-bar">
|
||||
{/* 图片预览 */}
|
||||
{imageUrl && (
|
||||
<div className="px-3 pt-3" data-alt="image-preview">
|
||||
{/* 媒体预览 */}
|
||||
<div className="px-3 pt-3 flex gap-2" data-alt="media-preview">
|
||||
{/* 图片预览 */}
|
||||
{imageUrl && (
|
||||
<div className="relative group w-24 h-24">
|
||||
<img
|
||||
src={imageUrl}
|
||||
@ -83,8 +113,33 @@ export function InputBar({ onSend }: InputBarProps) {
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* 视频预览 */}
|
||||
{videoUrl && (
|
||||
<div className="relative group w-24 h-24">
|
||||
<video
|
||||
src={videoUrl}
|
||||
className="h-full w-full object-cover rounded-xl border border-white/10"
|
||||
controls={false}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (setVideoPreview) {
|
||||
setVideoPreview(videoUrl!, videoId!);
|
||||
}
|
||||
setVideoUrl(null);
|
||||
setVideoId(null);
|
||||
}}
|
||||
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition bg-black/60 text-white rounded-full p-1"
|
||||
title="移除"
|
||||
data-alt="remove-video-button"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 上传进度 */}
|
||||
{isUploading && (
|
||||
@ -134,7 +189,7 @@ export function InputBar({ onSend }: InputBarProps) {
|
||||
onClick={handleSend}
|
||||
className="inline-flex items-center gap-2 p-2 my-2 rounded-full bg-blue-500 hover:bg-blue-400 text-white shadow disabled:bg-gray-500 disabled:hover:bg-gray-500 disabled:cursor-not-allowed"
|
||||
data-alt="send-button"
|
||||
disabled={!text.trim() && !imageUrl}
|
||||
disabled={!text.trim() && !imageUrl && !videoUrl}
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
|
||||
@ -13,6 +13,9 @@ interface SmartChatBoxProps {
|
||||
setIsSmartChatBoxOpen: (v: boolean) => void;
|
||||
projectId: string;
|
||||
userId: number;
|
||||
previewVideoUrl?: string | null;
|
||||
previewVideoId?: string | null;
|
||||
onClearPreview?: () => void;
|
||||
}
|
||||
|
||||
interface MessageGroup {
|
||||
@ -32,7 +35,15 @@ function BackToLatestButton({ onClick }: { onClick: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function SmartChatBox({ isSmartChatBoxOpen, setIsSmartChatBoxOpen, projectId, userId }: SmartChatBoxProps) {
|
||||
export default function SmartChatBox({
|
||||
isSmartChatBoxOpen,
|
||||
setIsSmartChatBoxOpen,
|
||||
projectId,
|
||||
userId,
|
||||
previewVideoUrl,
|
||||
previewVideoId,
|
||||
onClearPreview
|
||||
}: SmartChatBoxProps) {
|
||||
// 消息列表引用
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
@ -52,6 +63,11 @@ export default function SmartChatBox({ isSmartChatBoxOpen, setIsSmartChatBoxOpen
|
||||
checkIfAtBottom();
|
||||
}, [checkIfAtBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('previewVideoUrl', previewVideoUrl);
|
||||
console.log('previewVideoId', previewVideoId);
|
||||
}, [previewVideoUrl, previewVideoId]);
|
||||
|
||||
// 监听滚动事件
|
||||
useEffect(() => {
|
||||
const listElement = listRef.current;
|
||||
@ -170,7 +186,16 @@ export default function SmartChatBox({ isSmartChatBoxOpen, setIsSmartChatBoxOpen
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<InputBar onSend={sendMessage} />
|
||||
<InputBar
|
||||
onSend={sendMessage}
|
||||
setVideoPreview={(url, id) => {
|
||||
if (url === previewVideoUrl && id === previewVideoId) {
|
||||
onClearPreview?.();
|
||||
}
|
||||
}}
|
||||
initialVideoUrl={previewVideoUrl || undefined}
|
||||
initialVideoId={previewVideoId || undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -519,11 +519,13 @@ export async function fetchMessages(
|
||||
*/
|
||||
export async function sendMessage(
|
||||
blocks: MessageBlock[],
|
||||
config: ChatConfig
|
||||
config: ChatConfig,
|
||||
videoId?: string
|
||||
): Promise<void> {
|
||||
// 提取文本和图片
|
||||
// 提取文本、图片和视频
|
||||
const textBlocks = blocks.filter(b => b.type === "text");
|
||||
const imageBlocks = blocks.filter(b => b.type === "image");
|
||||
const videoBlocks = blocks.filter(b => b.type === "video");
|
||||
|
||||
const request: SendMessageRequest = {
|
||||
session_id: `project_${config.projectId}_user_${config.userId}`,
|
||||
@ -537,6 +539,16 @@ export async function sendMessage(
|
||||
request.image_url = (imageBlocks[0] as { url: string }).url;
|
||||
}
|
||||
|
||||
// 如果有视频,添加视频URL
|
||||
if (videoBlocks.length > 0) {
|
||||
request.video_url = (videoBlocks[0] as { url: string }).url;
|
||||
}
|
||||
|
||||
// 如果有视频ID,添加到请求中
|
||||
if (videoId) {
|
||||
request.video_id = videoId;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('发送消息请求:', request);
|
||||
await post<ApiResponse<RealApiMessage>>("/intelligent/chat", request);
|
||||
|
||||
@ -54,6 +54,8 @@ export interface SendMessageRequest {
|
||||
session_id: string;
|
||||
user_input: string;
|
||||
image_url?: string;
|
||||
video_id?: string;
|
||||
video_url?: string;
|
||||
project_id: string;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
@ -135,7 +135,7 @@ export function useMessages({ config, onMessagesUpdate }: UseMessagesProps): [Me
|
||||
}, [latestMessages, filterMessages, onMessagesUpdate]);
|
||||
|
||||
// 发送消息
|
||||
const handleSendMessage = useCallback(async (blocks: MessageBlock[]) => {
|
||||
const handleSendMessage = useCallback(async (blocks: MessageBlock[], videoId?: string) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
@ -166,7 +166,7 @@ export function useMessages({ config, onMessagesUpdate }: UseMessagesProps): [Me
|
||||
});
|
||||
|
||||
// 发送到服务器
|
||||
await sendMessage(blocks, configRef.current);
|
||||
await sendMessage(blocks, configRef.current, videoId);
|
||||
|
||||
// 立即获取最新的消息列表
|
||||
await updateMessages(false);
|
||||
|
||||
@ -26,6 +26,8 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
|
||||
const [activeEditTab, setActiveEditTab] = React.useState('1');
|
||||
const [isSmartChatBoxOpen, setIsSmartChatBoxOpen] = React.useState(true);
|
||||
const [previewVideoUrl, setPreviewVideoUrl] = React.useState<string | null>(null);
|
||||
const [previewVideoId, setPreviewVideoId] = React.useState<string | null>(null);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const episodeId = searchParams.get('episodeId') || '';
|
||||
@ -145,6 +147,11 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
mode={mode}
|
||||
onOpenChat={() => setIsSmartChatBoxOpen(true)}
|
||||
setVideoPreview={(url, id) => {
|
||||
setPreviewVideoUrl(url);
|
||||
setPreviewVideoId(id);
|
||||
}}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
@ -227,6 +234,12 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
setIsSmartChatBoxOpen={setIsSmartChatBoxOpen}
|
||||
projectId={episodeId}
|
||||
userId={userId}
|
||||
previewVideoUrl={previewVideoUrl}
|
||||
previewVideoId={previewVideoId}
|
||||
onClearPreview={() => {
|
||||
setPreviewVideoUrl(null);
|
||||
setPreviewVideoId(null);
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@ import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
|
||||
import { mockScriptData } from '@/components/script-renderer/mock';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Video } from 'lucide-react';
|
||||
|
||||
interface MediaViewerProps {
|
||||
taskObject: TaskObject;
|
||||
@ -22,6 +24,8 @@ interface MediaViewerProps {
|
||||
isPauseWorkFlow: boolean;
|
||||
applyScript: any;
|
||||
mode: string;
|
||||
onOpenChat?: () => void;
|
||||
setVideoPreview?: (url: string, id: string) => void;
|
||||
}
|
||||
|
||||
export const MediaViewer = React.memo(function MediaViewer({
|
||||
@ -35,7 +39,9 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
setAnyAttribute,
|
||||
isPauseWorkFlow,
|
||||
applyScript,
|
||||
mode
|
||||
mode,
|
||||
onOpenChat,
|
||||
setVideoPreview
|
||||
}: MediaViewerProps) {
|
||||
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const finalVideoRef = useRef<HTMLVideoElement>(null);
|
||||
@ -447,28 +453,47 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
|
||||
{/* 视频 多个 取第一个 */}
|
||||
{ taskObject.videos.data[currentSketchIndex].urls && (
|
||||
<motion.div
|
||||
initial={{ clipPath: "inset(0 100% 0 0)" }}
|
||||
animate={{ clipPath: "inset(0 0% 0 0)" }}
|
||||
transition={{ duration: 0.8, ease: [0.43, 0.13, 0.23, 0.96] }}
|
||||
className="relative z-10 w-full h-full"
|
||||
>
|
||||
<video
|
||||
ref={mainVideoRef}
|
||||
key={taskObject.videos.data[currentSketchIndex].urls[0]}
|
||||
className="w-full h-full rounded-lg object-cover object-center relative z-10"
|
||||
src={taskObject.videos.data[currentSketchIndex].urls[0]}
|
||||
autoPlay={isVideoPlaying}
|
||||
loop={true}
|
||||
playsInline
|
||||
onLoadedData={() => applyVolumeSettings(mainVideoRef.current!)}
|
||||
onEnded={() => {
|
||||
if (isVideoPlaying) {
|
||||
// 自动切换到下一个视频的逻辑在父组件处理
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ clipPath: "inset(0 100% 0 0)" }}
|
||||
animate={{ clipPath: "inset(0 0% 0 0)" }}
|
||||
transition={{ duration: 0.8, ease: [0.43, 0.13, 0.23, 0.96] }}
|
||||
className="relative z-10 w-full h-full"
|
||||
>
|
||||
<video
|
||||
ref={mainVideoRef}
|
||||
key={taskObject.videos.data[currentSketchIndex].urls[0]}
|
||||
className="w-full h-full rounded-lg object-cover object-center relative z-10"
|
||||
src={taskObject.videos.data[currentSketchIndex].urls[0]}
|
||||
autoPlay={isVideoPlaying}
|
||||
loop={true}
|
||||
playsInline
|
||||
onLoadedData={() => applyVolumeSettings(mainVideoRef.current!)}
|
||||
onEnded={() => {
|
||||
if (isVideoPlaying) {
|
||||
// 自动切换到下一个视频的逻辑在父组件处理
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* 添加到chat去编辑 按钮 */}
|
||||
<Tooltip title="Add to chat to edit">
|
||||
<Button
|
||||
className="absolute top-4 left-4 z-[21] bg-white/10 backdrop-blur-sm border border-white/20 text-white"
|
||||
onClick={() => {
|
||||
const currentVideo = taskObject.videos.data[currentSketchIndex];
|
||||
if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0 && setVideoPreview) {
|
||||
setVideoPreview(currentVideo.urls[0], currentVideo.video_id);
|
||||
if (onOpenChat) onOpenChat();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Video className="w-4 h-4" />
|
||||
<span className="text-xs">Chat to edit</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 操作按钮组 */}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user