chatbox 加入 添加分镜视频

This commit is contained in:
北枳 2025-08-25 19:46:49 +08:00
parent fef7e0ed26
commit 8a0474fbf8
8 changed files with 173 additions and 40 deletions

View File

@ -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}`);

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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);

View File

@ -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>

View File

@ -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>
</>
)}
{/* 操作按钮组 */}