diff --git a/.env.development b/.env.development index e1bc4f8..e37d8cc 100644 --- a/.env.development +++ b/.env.development @@ -4,7 +4,7 @@ NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com NEXT_PUBLIC_CUT_URL = https://77.smartcut.py.qikongjian.com # 失败率 -NEXT_PUBLIC_ERROR_CONFIG = 0.1 +NEXT_PUBLIC_ERROR_CONFIG = 0.5 # Google OAuth配置 NEXT_PUBLIC_GOOGLE_CLIENT_ID=847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com NEXT_PUBLIC_GOOGLE_REDIRECT_URI=https://www.movieflow.net/api/auth/google/callback diff --git a/api/DTO/movieEdit.ts b/api/DTO/movieEdit.ts index b0f4961..0e89685 100644 --- a/api/DTO/movieEdit.ts +++ b/api/DTO/movieEdit.ts @@ -367,6 +367,8 @@ export interface VideoFlowProjectResponse { final_simple_video: string; /** 最终视频 */ final_video: string; + /** 画面比例 */ + aspect_ratio: string; } /** * 新角色列表项接口 diff --git a/api/script_episode.ts b/api/script_episode.ts index e450b27..cc074fc 100644 --- a/api/script_episode.ts +++ b/api/script_episode.ts @@ -1,6 +1,7 @@ import { post } from './request'; import { ApiResponse } from './common'; import { Character } from './video_flow'; +import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector'; // 创建剧集的数据类型 export interface CreateScriptEpisodeRequest { @@ -78,6 +79,7 @@ interface MovieProject { last_message: string; updated_at: string; created_at: string; + aspect_ratio: AspectRatioValue; } interface ListMovieProjectsResponse { diff --git a/app/api/auth/google/callback/route.ts b/app/api/auth/google/callback/route.ts index 7558d6b..cb79f02 100644 --- a/app/api/auth/google/callback/route.ts +++ b/app/api/auth/google/callback/route.ts @@ -1,251 +1,17 @@ import { NextRequest, NextResponse } from 'next/server'; -import type { OAuthCallbackParams } from '@/app/types/google-oauth'; /** * Google OAuth回调处理API - * 处理从Google OAuth返回的授权码,完成用户认证 + * 注意:POST 方法已废弃,业务逻辑已移至 /users/oauth/callback 页面 + * 此文件现在仅用于处理 Google 的 GET 回调重定向 */ -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { code, state, inviteCode } = body; - - // 验证必需参数 - if (!code || !state) { - return NextResponse.json( - { - success: false, - message: 'Missing required parameters: code and state' - }, - { status: 400 } - ); - } - - // 开发模式:使用测试环境的OAuth处理 - const isDevelopment = process.env.NODE_ENV === 'development'; - const useTestEnv = isDevelopment; // 开发环境默认使用测试环境 - - if (useTestEnv) { - console.log('🧪 开发模式:使用模拟OAuth响应'); - - // 解析state参数获取origin信息 - let stateData: any = {}; - try { - stateData = JSON.parse(state); - } catch (e) { - console.warn('无法解析state参数:', state); - } - - // 模拟成功的OAuth响应 - const mockResponse = { - success: true, - data: { - token: 'dev-mock-token-' + Date.now(), - user: { - userId: 'dev-user-' + Math.random().toString(36).substr(2, 9), - userName: 'Development User', - name: 'Dev User', - email: 'dev@movieflow.com', - authType: 'GOOGLE', - isNewUser: false - }, - userInfo: { - userId: 'dev-user-' + Math.random().toString(36).substr(2, 9), - userName: 'Development User', - name: 'Dev User', - email: 'dev@movieflow.com', - authType: 'GOOGLE', - isNewUser: false - }, - message: 'Development mode - Google authentication simulated' - } - }; - - console.log('返回模拟OAuth响应:', mockResponse); - return NextResponse.json(mockResponse); - } - - // 解析state参数 - let stateData: any = {}; - try { - stateData = JSON.parse(state); - } catch (e) { - console.warn('无法解析state参数:', state); - return NextResponse.json( - { - success: false, - message: 'Invalid state parameter' - }, - { status: 400 } - ); - } - - console.log('Google OAuth回调处理开始:', { - codeLength: code.length, - stateData, - inviteCode - }); - - // 第一步:使用authorization code向Google换取access token和id_token - const googleClientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com'; - const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET || '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'; - - if (!googleClientSecret) { - console.error('Google Client Secret未配置'); - return NextResponse.json( - { - success: false, - message: 'Google Client Secret not configured' - }, - { status: 500 } - ); - } - - console.log('开始向Google交换token...'); - - // 创建fetch配置,包含超时和重试机制 - const fetchOptions = { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - code, - client_id: googleClientId, - client_secret: googleClientSecret, - redirect_uri: getRedirectUri(request), - grant_type: 'authorization_code', - }), - // 增加超时时间到30秒 - signal: AbortSignal.timeout(30000) - }; - - let tokenResponse; - try { - tokenResponse = await fetch('https://oauth2.googleapis.com/token', fetchOptions); - } catch (fetchError: any) { - console.error('Google API连接失败:', fetchError.message); - - // 如果是超时错误,提供更友好的错误信息 - if (fetchError.name === 'TimeoutError' || fetchError.code === 'UND_ERR_CONNECT_TIMEOUT') { - return NextResponse.json( - { - success: false, - message: 'Google authentication service is temporarily unavailable. Please check your network connection and try again.' - }, - { status: 503 } - ); - } - - throw fetchError; - } - - console.log('Google token exchange响应状态:', tokenResponse.status); - - if (!tokenResponse.ok) { - const errorText = await tokenResponse.text(); - console.error('Google token exchange failed:', errorText); - return NextResponse.json( - { - success: false, - message: 'Failed to exchange authorization code for tokens' - }, - { status: 400 } - ); - } - - const tokenData = await tokenResponse.json(); - console.log('Google token exchange成功,获得token'); - const { id_token } = tokenData; - - if (!id_token) { - console.error('No id_token received from Google'); - return NextResponse.json( - { - success: false, - message: 'No id_token received from Google' - }, - { status: 400 } - ); - } - - // 第二步:使用id_token调用Java后端 - const javaBaseUrl = process.env.NEXT_PUBLIC_JAVA_URL || 'https://auth.test.movieflow.ai'; - console.log('开始调用Java后端:', javaBaseUrl); - - const backendResponse = await fetch(`${javaBaseUrl}/api/auth/google/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify({ - idToken: id_token, // 使用从Google获取的id_token - action: 'auto', // 自动判断登录或注册 - inviteCode: inviteCode || stateData.inviteCode || undefined - }) - }); - - console.log('Java后端响应状态:', backendResponse.status); - const backendResult = await backendResponse.json(); - - if (!backendResponse.ok || !backendResult.success) { - console.error('Java后端处理Google OAuth失败:', backendResult); - return NextResponse.json( - { - success: false, - message: backendResult.message || 'Google authentication failed' - }, - { status: backendResponse.status || 500 } - ); - } - - console.log('Google OAuth认证成功:', { - userId: backendResult.data?.user?.userId, - email: backendResult.data?.user?.email - }); - - // 返回成功结果 - return NextResponse.json({ - success: true, - data: { - token: backendResult.data.token, - user: backendResult.data.userInfo || backendResult.data.user, - message: 'Google authentication successful' - } - }); - - } catch (error: any) { - console.error('Google OAuth回调处理错误:', error); - - // 检查是否是网络连接错误 - if (error.code === 'ECONNREFUSED' || error.message?.includes('fetch failed')) { - console.error('网络连接失败,可能是Java后端服务不可用'); - return NextResponse.json( - { - success: false, - message: 'Backend service unavailable. Please try again later.' - }, - { status: 503 } - ); - } - - return NextResponse.json( - { - success: false, - message: error.message || 'Internal server error during Google OAuth callback' - }, - { status: 500 } - ); - } -} /** * 根据请求获取正确的redirect_uri */ function getRedirectUri(request: NextRequest): string { const host = request.headers.get('host') || ''; - + if (host.includes('localhost') || host.includes('127.0.0.1')) { // 本地开发环境:使用实际的端口号(可能是3000或3001) const protocol = 'http'; @@ -268,30 +34,30 @@ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const code = searchParams.get('code'); const state = searchParams.get('state'); - + if (!code || !state) { return NextResponse.json( - { - success: false, - message: 'Missing required parameters: code and state' - }, - { status: 400 } + { + success: false, + message: 'Missing required parameters: code and state' + }, + { status: 400 } ); } - + // 重定向到页面路由,让页面处理OAuth回调 const callbackUrl = `/users/oauth/callback?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`; - + // 修复:确保使用正确的域名进行重定向 const host = request.headers.get('host') || 'www.movieflow.net'; const protocol = request.headers.get('x-forwarded-proto') || 'https'; const fullCallbackUrl = `${protocol}://${host}${callbackUrl}`; - + console.log('🔍 前端API重定向调试:'); console.log(' - request.url:', request.url); console.log(' - host header:', host); console.log(' - protocol:', protocol); console.log(' - 重定向到:', fullCallbackUrl); - + return NextResponse.redirect(fullCallbackUrl); } diff --git a/app/share/page.tsx b/app/share/page.tsx index 66c97cd..37e2b83 100644 --- a/app/share/page.tsx +++ b/app/share/page.tsx @@ -151,7 +151,7 @@ export default function SharePage(): JSX.Element { const toggleRow = React.useCallback((id: string) => { - setExpandedRowIds((prev) => { + setExpandedRowIds((prev: Set) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); @@ -185,45 +185,62 @@ export default function SharePage(): JSX.Element { Step 1 Share -

Copy your invitation code and share it with friends.

+

Copy your invitation link and share it with friends.

  • Step 2 Register
    -

    Friends register and enter your invitation code.

    +

    Friends click the link and register directly.

  • Step 3 Reward
    -

    You both receive rewards after successful registration.

    +

    You both receive rewards after your friend activates their account.

  • - {/* Section 2: My Invitation Code */} -
    -
    -
    -

    My Invitation Code

    + {/* Section 2: My Invitation Link */} +
    +
    +
    +

    My Invitation Link

    -
    - {inviteCode} +
    +
    + {inviteCode ? `${'https://www.movieflow.ai'}/signup?inviteCode=${inviteCode}` : '-'} +
    + {/* 右侧渐变遮挡 */} +
    -

    Share this code. Your friends can enter it during registration.

    +

    Share this link. Your friends can register directly through it.

    Total Credits @@ -233,7 +250,7 @@ export default function SharePage(): JSX.Element {
    Invited Friends {invitedCount} - Points detail will be available soon. + Point details will be available soon.
    @@ -245,9 +262,9 @@ export default function SharePage(): JSX.Element {
    {/* 右侧Action按钮 */} diff --git a/components/ChatInputBox/H5PhotoStoryDrawer.tsx b/components/ChatInputBox/H5PhotoStoryDrawer.tsx index 481f1f3..b13e0d4 100644 --- a/components/ChatInputBox/H5PhotoStoryDrawer.tsx +++ b/components/ChatInputBox/H5PhotoStoryDrawer.tsx @@ -359,12 +359,12 @@ export const H5PhotoStoryDrawer = ({
    - {/* 横/竖屏选择 上线暂时不开放 */} - {/* */} + /> {!hasAnalyzed ? (
    diff --git a/components/ChatInputBox/H5TemplateDrawer.tsx b/components/ChatInputBox/H5TemplateDrawer.tsx index 394951e..562b406 100644 --- a/components/ChatInputBox/H5TemplateDrawer.tsx +++ b/components/ChatInputBox/H5TemplateDrawer.tsx @@ -556,12 +556,12 @@ export const H5TemplateDrawer = ({ />
    )} - {/* 横/竖屏选择 上线暂时不开放 */} - {/* */} + /> 0} handleCreateVideo={handleConfirm} diff --git a/components/ChatInputBox/PcPhotoStoryModal.tsx b/components/ChatInputBox/PcPhotoStoryModal.tsx index 0f139a1..989fd08 100644 --- a/components/ChatInputBox/PcPhotoStoryModal.tsx +++ b/components/ChatInputBox/PcPhotoStoryModal.tsx @@ -321,12 +321,12 @@ export const PcPhotoStoryModal = ({ placeholder="Share your creative ideas about the image and let AI create a movie story for you..." />
    - {/* 横/竖屏选择 上线暂时不开放 */} - {/* */} + /> {!hasAnalyzed ? (
    )} - {/* 横/竖屏选择 上线暂时不开放 */} - {/* */} + /> 0} handleCreateVideo={handleConfirm} diff --git a/components/SmartChatBox/InputBar.tsx b/components/SmartChatBox/InputBar.tsx index 5ff55b4..ecea67d 100644 --- a/components/SmartChatBox/InputBar.tsx +++ b/components/SmartChatBox/InputBar.tsx @@ -4,6 +4,8 @@ import { MessageBlock } from "./types"; import { useUploadFile } from "@/app/service/domain/service"; import { motion, AnimatePresence } from "framer-motion"; import { QuickActionTags, QuickAction } from "./QuickActionTags"; +import { useDeviceType } from '@/hooks/useDeviceType'; + // 防抖函数 function debounce void>(func: T, wait: number) { @@ -35,6 +37,7 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide const [videoUrl, setVideoUrl] = useState(initialVideoUrl || null); const [videoId, setVideoId] = useState(initialVideoId || null); const [isMultiline, setIsMultiline] = useState(false); +const { isMobile, isTablet, isDesktop } = useDeviceType(); const textareaRef = useRef(null); const { uploadFile } = useUploadFile(); @@ -174,7 +177,7 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide }; return ( -
    +
    {/* 媒体预览 */}
    {/* 图片预览 */} diff --git a/components/SmartChatBox/MessageRenderer.tsx b/components/SmartChatBox/MessageRenderer.tsx index cd99578..84a1715 100644 --- a/components/SmartChatBox/MessageRenderer.tsx +++ b/components/SmartChatBox/MessageRenderer.tsx @@ -5,6 +5,7 @@ import { bubbleVariants, hhmm } from "./utils"; import { ProgressBar } from "./ProgressBar"; import { Loader2, AlertCircle, CheckCircle2 } from "lucide-react"; import { Image } from 'antd'; +import { useDeviceType } from '@/hooks/useDeviceType'; interface MessageRendererProps { msg: ChatMessage; @@ -15,6 +16,7 @@ export function MessageRenderer({ msg, sendMessage }: MessageRendererProps) { // Decide bubble style const isUser = msg.role === "user"; const isSystem = msg.role === "system"; + const { isMobile, isTablet, isDesktop } = useDeviceType(); const bubbleClass = useMemo(() => { if (isSystem) return "bg-[#281c1459] text-white"; @@ -78,7 +80,7 @@ export function MessageRenderer({ msg, sendMessage }: MessageRendererProps) { data-alt="message-bubble" key={msg.id} > -
    +
    {/* Header */} {/*
    {badge} diff --git a/components/SmartChatBox/SmartChatBox.tsx b/components/SmartChatBox/SmartChatBox.tsx index a8ec19e..7ba47fe 100644 --- a/components/SmartChatBox/SmartChatBox.tsx +++ b/components/SmartChatBox/SmartChatBox.tsx @@ -7,6 +7,7 @@ import { useMessages } from "./useMessages"; import { DateDivider } from "./DateDivider"; import { LoadMoreButton } from "./LoadMoreButton"; import { ChatMessage } from "./types"; +import { useDeviceType } from '@/hooks/useDeviceType'; interface SmartChatBoxProps { isSmartChatBoxOpen: boolean; @@ -51,7 +52,7 @@ export default function SmartChatBox({ // 消息列表引用 const listRef = useRef(null); const [isAtBottom, setIsAtBottom] = useState(true); - + const { isMobile, isTablet, isDesktop } = useDeviceType(); // 检查是否滚动到底部 const checkIfAtBottom = useCallback(() => { if (listRef.current) { @@ -148,18 +149,18 @@ export default function SmartChatBox({ }, [messages]); return ( -
    +
    {/* Header */} -
    +
    Chat {/* System push toggle */}
    diff --git a/components/pages/create-to-video2.tsx b/components/pages/create-to-video2.tsx index 3c38f09..9635982 100644 --- a/components/pages/create-to-video2.tsx +++ b/components/pages/create-to-video2.tsx @@ -199,13 +199,13 @@ export default function CreateToVideo2() { onMouseLeave={() => handleMouseLeave(project.project_id)} data-alt="project-card" > - {/* 视频/图片区域 */} -
    router.push(`/movies/work-flow?episodeId=${project.project_id}`)}> + {/* 视频/图片区域 */} +
    router.push(`/movies/work-flow?episodeId=${project.project_id}`)}> {(project.final_video_url || project.final_simple_video_url || project.video_urls) ? (
    )} @@ -611,6 +618,14 @@ Please process this video editing request.`;
    + {isMobile ? ( + setIsSmartChatBoxOpen(true)} + className="backdrop-blur-lg" + /> + ) : ( + )}
    {/* 智能对话弹窗 */} setIsSmartChatBoxOpen(false)} > diff --git a/components/pages/work-flow/H5MediaViewer.tsx b/components/pages/work-flow/H5MediaViewer.tsx index b9ffd7d..42e4655 100644 --- a/components/pages/work-flow/H5MediaViewer.tsx +++ b/components/pages/work-flow/H5MediaViewer.tsx @@ -43,6 +43,12 @@ interface H5MediaViewerProps { onRetryVideo?: (video_id: string) => void; /** 切换选择视图(final 或 video) */ onSelectView?: (view: 'final' | 'video') => void; + /** 启用视频编辑功能 */ + enableVideoEdit?: boolean; + /** 视频编辑描述提交回调 */ + onVideoEditDescriptionSubmit?: (editPoint: any, description: string) => void; + /** 项目ID */ + projectId?: string; } /** @@ -67,7 +73,10 @@ export function H5MediaViewer({ onGotoCut, isSmartChatBoxOpen, onRetryVideo, - onSelectView + onSelectView, + enableVideoEdit, + onVideoEditDescriptionSubmit, + projectId }: H5MediaViewerProps) { const carouselRef = useRef(null); const videoRefs = useRef>([]); @@ -155,7 +164,7 @@ export function H5MediaViewer({ // 渲染视频 slide const renderVideoSlides = () => ( -
    +
    ) : ( -
    +
    {status === 0 && ( Generating... )} @@ -276,7 +285,7 @@ export function H5MediaViewer({ // 渲染图片 slide const renderImageSlides = () => ( -
    +
    {imageUrls.map((url, idx) => (
    - scene + scene
    ))}
    @@ -407,7 +416,7 @@ export function H5MediaViewer({ data-alt="final-thumb-item" aria-label="Select final video" > - final + final
    Final
    diff --git a/components/pages/work-flow/media-viewer.tsx b/components/pages/work-flow/media-viewer.tsx index 9736e5b..0cafa86 100644 --- a/components/pages/work-flow/media-viewer.tsx +++ b/components/pages/work-flow/media-viewer.tsx @@ -2,7 +2,7 @@ import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow, CircleAlert /*, PenTool*/ } from 'lucide-react'; +import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow, CircleAlert, PenTool } from 'lucide-react'; import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal'; import { GlassIconButton } from '@/components/ui/glass-icon-button'; import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer'; @@ -12,9 +12,8 @@ import ScriptLoading from './script-loading'; import { TaskObject } from '@/api/DTO/movieEdit'; import { Button, Tooltip } from 'antd'; import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools'; -// 临时禁用视频编辑功能 -// import { VideoEditOverlay } from './video-edit/VideoEditOverlay'; -// import { EditPoint as EditPointType } from './video-edit/types'; +import { VideoEditOverlay } from './video-edit/VideoEditOverlay'; +import { EditPoint as EditPointType } from './video-edit/types'; interface MediaViewerProps { taskObject: TaskObject; @@ -35,9 +34,9 @@ interface MediaViewerProps { onGotoCut: () => void; isSmartChatBoxOpen: boolean; onRetryVideo?: (video_id: string) => void; - // 临时禁用视频编辑功能 - // enableVideoEdit?: boolean; - // onVideoEditDescriptionSubmit?: (editPoint: EditPointType, description: string) => void; + enableVideoEdit?: boolean; + onVideoEditDescriptionSubmit?: (editPoint: EditPointType, description: string) => void; + projectId?: string; } export const MediaViewer = React.memo(function MediaViewer({ @@ -58,10 +57,10 @@ export const MediaViewer = React.memo(function MediaViewer({ showGotoCutButton, onGotoCut, isSmartChatBoxOpen, - onRetryVideo - // 临时禁用视频编辑功能 - // enableVideoEdit = true, - // onVideoEditDescriptionSubmit + onRetryVideo, + enableVideoEdit = true, + onVideoEditDescriptionSubmit, + projectId }: MediaViewerProps) { const mainVideoRef = useRef(null); const finalVideoRef = useRef(null); @@ -78,8 +77,7 @@ export const MediaViewer = React.memo(function MediaViewer({ const [toosBtnRight, setToodsBtnRight] = useState('1rem'); const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false); const [isLoadingDownloadAllVideosBtn, setIsLoadingDownloadAllVideosBtn] = useState(false); - // 临时禁用视频编辑功能 - // const [isVideoEditMode, setIsVideoEditMode] = useState(false); + const [isVideoEditMode, setIsVideoEditMode] = useState(false); useEffect(() => { if (isSmartChatBoxOpen) { @@ -178,7 +176,7 @@ export const MediaViewer = React.memo(function MediaViewer({ return (