resolve: 合并AI剪辑功能与远程更新

- 保留AI剪辑功能的所有代码
- 采用远程版本的UI布局改进
- 统一使用Tooltip包装按钮
- 调整按钮位置为fixed定位
- 合并import语句,同时导入Drawer和Tooltip
This commit is contained in:
qikongjian 2025-08-31 01:45:22 +08:00
commit c6f1a79743
63 changed files with 2402 additions and 1414 deletions

View File

@ -680,10 +680,6 @@ export interface TaskObject {
data: Scene[];
total_count: number;
}; // 场景
shot_sketch: {
data: ShotSketch[];
total_count: number;
}; // 分镜草图
videos: {
data: ShotVideo[];
total_count: number;

View File

@ -210,33 +210,33 @@ export interface CreateMovieProjectV3Request {
language: string;
/**模板id */
template_id: string;
/**故事角色 */
/** 故事角色 */
storyRole: {
/**角色名 */
/** 角色名 */
role_name: string;
/**角色描述 */
role_description: {
name: string;
image_url: string;
character_analysis: Record<string, any>;
};
/**照片URL */
/** 角色描述ai分析出来用于剧本生成 */
role_description: string;
/** 用户提示,提示给用户需要输入什么内容 */
user_tips: string;
/** 约束,可选,用于传给ai让ai去拦截用户不符合约束的输入内容 */
constraints: string;
/** 照片URL */
photo_url: string;
/**声音URL */
/** 声音URL */
voice_url: string;
}[];
/** 可填写的变量字段 */
fillable_content?: {
/** 字段名称 */
field_name: string;
/** 字段类型 */
field_type: string;
/** 字段值 */
field_value?: string;
/** 字段描述 */
field_description?: string;
/** 字段元数据 */
field_meta?: Record<string, any>;
/** 道具 */
storyItem: {
/** 道具名 */
item_name: string;
/** 道具描述ai分析出来用于剧本生成 */
item_description: string;
/** 用户提示,提示给用户需要输入什么内容 */
user_tips: string;
/** 约束,可选,用于传给ai让ai去拦截用户不符合约束的输入内容 */
constraints: string;
/** 道具照片URL */
photo_url: string;
}[];
}

View File

@ -1,3 +1,4 @@
export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL
// export const BASE_URL = 'https://77.smartvideo.py.qikongjian.com'
// export const BASE_URL ='http://192.168.120.5:8000'
//

View File

@ -183,6 +183,28 @@ export type ConvertScenePromptRequest =
// 转换分镜头响应接口
export type ConvertScenePromptResponse = BaseApiResponse<ScenePrompts>;
// 生成剪辑计划请求接口
interface GenerateEditPlanRequest {
project_id: string;
}
interface GenerateEditPlanResponseData {
project_id: string;
director_intent: string;
success: boolean;
editing_plan: any;
error: string | null;
processing_time: number;
video_count: number;
from_cache: boolean;
total_videos: number;
analyzed_count: number;
unanalyzed_videos: string[];
}
// 生成剪辑计划响应接口
export type GenerateEditPlanResponse = BaseApiResponse<GenerateEditPlanResponseData>;
/**
*
* @param request - project_type
@ -269,6 +291,11 @@ export const getRunningStreamData = async (data: {
return post<ApiResponse<any>>("/movie/get_status", data);
};
// 获取 生成剪辑计划 接口
export const getGenerateEditPlan = async (data: GenerateEditPlanRequest): Promise<ApiResponse<GenerateEditPlanResponse>> => {
return post<ApiResponse<GenerateEditPlanResponse>>("/edit-plan/generate-by-project", data);
};
/**
*
* @param data -

114
app/Privacy/page.tsx Normal file
View File

@ -0,0 +1,114 @@
import React from 'react'
export default function PrivacyPage() {
return (
<div className="h-screen overflow-y-auto bg-gray-50 text-black">
<div className="container mx-auto px-4 py-8 max-w-4xl">
<header className="mb-8">
<h1 className="text-3xl font-bold text-center mb-4">Movie Flow Privacy Policy</h1>
<p className="text-lg text-center text-gray-600">Effective Date: August 29, 2025</p>
</header>
<div className="bg-white rounded-lg shadow-lg p-8">
{/* Introduction */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">Introduction</h2>
<p className="mb-3">E&T Kingdom Co., Limited (together with our affiliates, the "Company", "us", "we", or "our") respect your privacy and are strongly committed to keeping secure any information we obtain from you or about you.</p>
<p className="mb-3">This Privacy Policy (this "Policy") applies to your use of Movie Flow, which includes its associated software applications and websites (all together, the "Services", "Movie Flow", or the "Platform"). In this Policy, "you" or "your" refers to any user who purchase or use any of the Services, including any user of the Platform.</p>
<p className="mb-3">This Privacy Policy describes our practices with respect to, and the basis on which we handle, your personal data which we collect from or about you. Your personal data refers to data, whether true or not, about you which can be identified either (a) from that data or (b) from that data and other information to which we have or are likely to have access ("Data").</p>
<p className="mb-3">By using the Services, including accessing or using the Platform, contacting or interacting with us or submitting information to us, you agree to the terms of this Policy and to the collection, use, disclosure and processing of your Data in accordance with this Policy.</p>
</section>
{/* Section 1: Data We Collect */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">1. Data We Collect</h2>
<p className="mb-3">We may collect a variety of Data from or about you or your devices from various sources, as described below. The types of Data we collect depend on how you use the Services. The Services offer optional features which, if used by you, require us to collect optional data to provide such features. You will be notified of such collection, as appropriate. If you do not provide your Data when requested, you may not be able to use our Services if that Data is necessary to provide you with our Services or if we are legally required to collect such Data.</p>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">A. Personal Data You Provide</h3>
<div className="mb-3">
<h4 className="text-lg font-semibold mb-2">(1) Registration and Contact Information</h4>
<p className="mb-3">When you register to use our Services, you may be required to provide certain Data including email address, username, date of birth, and age to create an account on the Platform ("Account"). The first time you log in Movie Flow using email verification, an Account of our platform will be provided for you. You can access the Services by logging in with your Account. When you sign-up or log-in to the Platform using a third-party service such as Facebook or Google, that service may provide us with information such as your username, email, and profile picture.</p>
</div>
<div className="mb-3">
<h4 className="text-lg font-semibold mb-2">(2) User Content</h4>
<p className="mb-3">We store and process the content you create, upload, generate, or access through the Platform, including text, audio, photos, videos, prompts, comments, feedback and any other content you upload, as well as interactions with the Platform (collectively "User Content"). When you create User Content, we may upload or import it to the Platform before you save or post the User Content (also known as pre-uploading).</p>
<p className="mb-3">When you use our Services, we may collect information about the images, videos, and audio that are part of your User Content, including identifying the objects and scenery that appear, the existence and nature of the audio, and the text transcript of the words spoken in your User Content to enable special video effects, for content moderation purposes, and other operations that will not identify any individual.</p>
<p className="mb-3">When you use our AI-generated content services or features to create content containing portraits (such as Character Face Mode), we may analyze the material you upload and extract the feature points (such as vector points of eyes, nose, mouth, etc.) and contour lines in the input material for portrait processing.</p>
</div>
<div className="mb-3">
<h4 className="text-lg font-semibold mb-2">(3) Financial Information</h4>
<p className="mb-3">When you use our paid Services, including redeeming voucher codes or purchasing paid Services, we collect information related to payment transactions (with your consent). This may include payment card details (e.g., card account number), billing information, order details, the Services you purchased, and transaction records regarding transactions, transfers, orders, withdrawals, rewards, tipping and/or other ID information necessary to identify users/accounts. We collect this information to process payments and ensure the security of all transactions.</p>
</div>
<div className="mb-3">
<h4 className="text-lg font-semibold mb-2">(4) Communication Information</h4>
<p className="mb-3">If you communicate with us, we will collect your contact information, such as email address, and the contents of any messages you send to us. This includes information in correspondence you send to us, including information we need to verify your identity or age, feedback or inquiries, and information about possible violations of our Terms of Service or other policies.</p>
</div>
<div className="mb-3">
<h4 className="text-lg font-semibold mb-2">(5) Business Cooperation Information</h4>
<p className="mb-3">With your explicit consent, we collect certain information when you request additional information about us or our Services, subscribing to marketing or otherwise contacting us. This may include: (i) contact information (such as your name, address, telephone number and email address) as well as the nature of your communication; (ii) professional information (such as your company name, job title); (iii) marketing information (such as your contact preferences, source or campaign details); and (iv) any information you choose to provide to us (for example, for event sign-up or bot interaction).</p>
</div>
<div className="mb-3">
<h4 className="text-lg font-semibold mb-2">(6) Other Information</h4>
<p className="mb-3">We may also collect your feedback about the Platform if you choose to provide it, such as information you share through surveys or your participation in challenges, research, promotions, marketing campaigns, events, or contests such as your gender, age, likeness, contact details and preferences.</p>
<p className="mb-3">When you use our Services, you may provide us with certain Data, including metadata accompanying user-generated content, audiovisual clips segmentation, annotation, frame or text extraction, object and image recognition symbolling, aggregated classification tagging and other non-personally identifying information inferred from the Data you provide to us or that we collect during your use of Movie Flow. We may process the above Data locally on your device to enable special video effects, and it will be used solely to provide the mentioned features. We do not analyze any facial recognition features in pictures or identify individuals from such pictures, or collect, share or store any face data. The above Data cannot be used to identify any individual.</p>
</div>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">B. Personal Data We Automatically Collect</h3>
<div className="mb-3">
<h4 className="text-lg font-semibold mb-2">(1) Technical Information</h4>
<p className="mb-3">We will collect information regarding the device you are using to access the Platform, such as IP address, operating system, browser you are using, your Account's preference settings, advertising identifiers, WiFi/WLAN/Bluetooth, mobile network information, your computer's or mobile device's operating system type and version, manufacturer and model, browser type, screen resolution, RAM and disk size, CPU usage, language settings, and app and file names. We automatically assign you a user ID and a device ID. Where you log-in from multiple devices, we will be able to use your profile information to identify your activity across devices.</p>
</div>
<div className="mb-3">
<h4 className="text-lg font-semibold mb-2">(2) Usage and Activity Information</h4>
<p className="mb-3">When you use our Services, we may automatically collect data about your interactions with Movie Flow and other users of Movie Flow. This includes your choices, follow history, followers, likes or participation records in challenges, surveys, contests, or other activities provided through Movie Flow. We collect information about how you engage with the Platform, including the content you view on the Platform, the pages or screens you viewed, the time spent on each page or screen, your browsing history, navigation paths, activity on specific pages, access timings, duration of access, the URL of the website from which you came to our site, and whether you opened our marketing emails or clicked on links within them.</p>
</div>
<div className="mb-3">
<h4 className="text-lg font-semibold mb-2">(3) Location Information</h4>
<p className="mb-3">We automatically collect data about your approximate location, such as country, state or city, based on your SIM card or IP address.</p>
</div>
<div className="mb-3">
<h4 className="text-lg font-semibold mb-2">(4) Cookies and Analytics</h4>
<p className="mb-3">We may collect your Data by using website/app cookies, in order to operate and administer our Services, and improve your user experience. A "cookie" is a piece of information sent to your browser by a website/app you visit. You can set your browser to accept all cookies, to reject all cookies, or to notify you whenever a cookie is offered so that you can decide each time whether to accept it. However, refusing a cookie may in some cases preclude you from using, or negatively affect the display or function of, a website/app or certain areas or features of a website/app.</p>
<p className="mb-3">We may use a variety of online analytics products that use cookies to help us analyze how users use our Services and to enhance your experience when you use our Services.</p>
</div>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">C. Information From Other Sources</h3>
<div className="mb-3">
<h4 className="text-lg font-semibold mb-2">(1) Our Corporate Group</h4>
<p className="mb-3">We may obtain information about you from certain affiliated entities within our corporate group, including about your activities on their platforms.</p>
</div>
<div className="mb-3">
<h4 className="text-lg font-semibold mb-2">(2) Third Parties</h4>
<p className="mb-3">We may receive information about you from organizations, businesses, people and others, including for example, publicly available sources, government authorities, and professional organizations. We also collect information about you where you are included or mentioned in User Content, in a complaint, appeal, request or feedback submitted by a user or third party, or if your contact information is provided to us by a user.</p>
</div>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">D. Combination of Data</h3>
<p className="mb-3">By using our Services, you agree that we may combine the Data we receive from and about you, including the Data you provide and the Data we automatically collect as mentioned above, as well as Data we may receive from third parties to help us tailor our communication to you and to improve our Services. If you do not agree to this section, please do not use the Platform or any of the Services.</p>
<p className="mb-3">The categories and scope of data we collect about you may vary from time to time, depending on your choice of and interaction with Movie Flow, as well as the country or region where you use Movie Flow. You will be notified of such a variation, as appropriate.</p>
<p className="mb-3">Where features are available, you may choose to browse certain content in the platform without logging in an account or registering, in which case we may still collect certain Data, including the content you browse, IP address, your interactions with the platform, and associated your device information such as device ID, WiFi information.</p>
</div>
</section>
</div>
</div>
</div>
);
}

483
app/Terms/page.tsx Normal file
View File

@ -0,0 +1,483 @@
import React from 'react'
export default function TermsPage() {
return (
<div className="h-screen overflow-y-auto bg-gray-50 text-black">
<div className="container mx-auto px-4 py-8 max-w-4xl">
<header className="mb-8">
<h1 className="text-3xl font-bold text-center mb-4">Movie Flow Terms of Service</h1>
<p className="text-lg text-center text-gray-600">Effective Date: August 29, 2025</p>
</header>
<div className="bg-white rounded-lg shadow-lg p-8">
<p className="mb-6 text-lg">Thank you for using Movie Flow!</p>
{/* Section 1: ACCEPTANCE OF THESE TERMS OF SERVICE */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">1. ACCEPTANCE OF THESE TERMS OF SERVICE</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">1.1 Agreement Overview</h3>
<p className="mb-3">These Terms of Service, including our Privacy Policy, other applicable terms, agreements, rules, guidelines, policies and notices, which are incorporated herein by reference (together, this "Agreement"), as may be amended from time to time, constitute a legally binding contract between you and E&T Kingdom Co., Limited and its affiliates (collectively, "Company," "we," "us," or "our"). This Agreement governs your use of Movie Flow, a platform that allows users to generate video content from prompts using artificial intelligence, along with its associated software applications and websites (collectively, the "Services" or "Platform"). References to "User," "you", and "your" refer to the individual accepting this Agreement, placing an order, creating an Account, or otherwise using the Services.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">1.2 Service Description</h3>
<p className="mb-3">The Services provide features and functionalities that allow users to create, modify, share, and otherwise use video content generated through the use of generative artificial intelligence (AI) technology.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">1.3 Acceptance and Binding Agreement</h3>
<p className="mb-3">IF YOU USE MOVIE FLOW OR THE SERVICES, WHETHER IN WHOLE OR IN PART, YOU ARE DEEMED TO (A) REPRESENT AND WARRANT THAT YOU ARE LAWFULLY ABLE TO BE BOUND BY; AND (B) AGREE TO BE BOUND BY AND ACCEPT THIS AGREEMENT AND OTHER RELATED DOCUMENTS THAT ARE EXPRESSLY INCORPORATED INTO THIS AGREEMENT BY REFERENCE. By accessing or using our Services, you confirm that you accept and agree to comply with these Terms. You understand and agree that we will treat your access or use of the Services as acceptance of these Terms from that point onwards.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">1.4 Business Entity Use</h3>
<p className="mb-3">If you are using our Services on behalf of a company, association, partnership, organization or other entity, whether in part or in whole, then you agree, represent, warrant and undertake that (a) "you," "your," and "Customer" includes you and the company, association, partnership, organization or other entity that you represent; (b) you are duly authorized by, and will remain authorized by, such entity to agree on its behalf and bind such entity to this Agreement; and (c) the entity is legally responsible for your use of the Services as well as for the use of your Account by any other individual authorized by such entity, including without limitation any officers, directors, employees, agents and advisors of such entity.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">1.5 Important Legal Notices</h3>
<p className="mb-3">YOU MUST CAREFULLY READ AND FULLY UNDERSTAND THIS AGREEMENT, INCLUDING ANY TERMS THAT MAY EXEMPT OR RESTRICT THE COMPANY'S LIABILITIES AND/OR RESPONSIBILITIES AND ANY TERMS THAT MAY RESTRICT AND/OR WAIVE YOUR RIGHTS, AS THEY APPLY TO YOUR USE OF THE SERVICES. THIS AGREEMENT CONTAINS A DISPUTE RESOLUTION AND ARBITRATION PROVISION THAT AFFECTS YOUR RIGHTS UNDER THIS AGREEMENT AND WITH RESPECT TO DISPUTES YOU MAY HAVE WITH THE COMPANY. YOUR USE OF THE SERVICES IS CONDITIONAL ON YOUR ACCEPTANCE OF THIS AGREEMENT. IF YOU DO NOT AGREE WITH OR ACCEPT THIS AGREEMENT, YOU SHALL NOT USE MOVIE FLOW OR ANY OF THE OTHER SERVICES.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">1.6 Modifications to Terms</h3>
<p className="mb-3">In order to provide better services or for legal, regulatory or security reasons, we reserve the right to amend, replace and/or otherwise update this Agreement from time to time. We will use commercially reasonable endeavors to notify you of any material modifications to this Agreement through the Platform, messages, or emails. You shall review this Agreement regularly to check for such modifications. If you do not agree to or accept the modifications to this Agreement, you will have to stop using Movie Flow or any of the Services. By continuing to use Movie Flow or any of the Services after the terms of this Agreement have been modified, you are deemed to have accepted the modifications.</p>
<p className="mb-3">ARBITRATION NOTICE. You agree that disputes arising under this Agreement will be resolved by binding, individual arbitration as specified below, and BY ACCEPTING THIS AGREEMENT, YOU AND MOVIE FLOW ARE EACH WAIVING THE RIGHT TO A TRIAL BY JURY OR TO PARTICIPATE IN ANY CLASS ACTION OR REPRESENTATIVE PROCEEDING.</p>
</div>
</section>
{/* Section 2: ELIGIBILITY AND ACCOUNT REGISTRATION */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">2. ELIGIBILITY AND ACCOUNT REGISTRATION</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">2.1 Age Requirements</h3>
<p className="mb-3">(a) Anyone under the age of 18 or other minimum age as defined under applicable laws of your jurisdiction ("Minimum Age") shall not use the Services.</p>
<p className="mb-3">(b) If you have reached the Minimum Age but are under the age of majority as defined under applicable laws of your jurisdiction ("Majority Age"), you may only use and register for a Movie Flow Account with the consent of your parent or legal guardian, and your parent or legal guardian hereby represents you and accepts this Agreement.</p>
<p className="mb-3">(c) Where parental consent or authorization is required under such applicable laws and regulations, you have the obligation to provide to us evidence of such consent or authorization, including as required under applicable laws and regulations, the consent or authorization of the holder of parental responsibility for the minor, including but not limited to agreeing to the following: (i) all the minor's actions in connection with their access to the Services; (ii) any fees or charges associated with the minor's use of any of the Services (as applicable); (iii) the minor's compliance with this Agreement; (iv) ensuring that any of the minor's participation in Services will not, in any event, result in any violation of applicable laws and regulations relating to child protections. We may refuse to process or continue to process the minor's personal information, or provide or continue to provide the Services to the minor until we receive this evidence of consent or authorization.</p>
<p className="mb-3">(d) If you are a minor in your country or region, your use of the Services may be subject to further age restrictions, whether imposed by us or any third party vendor in connection with the provision of certain Services. You may be unable to use or only have limited access to those Services, such as participating in rewards programs, top-up and tipping, without the assistance of your parent or legal guardian.</p>
<p className="mb-3">(e) If you learn that a child below the Minimum Age has registered for a Movie Flow Account or an Account of a child below the Majority Age was not registered under proper representation or guardianship, you may alert us at support@movieflow.ai. We will promptly verify, take steps to remove such Account information from Movie Flow and delete the Account.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">2.2 Account Creation and Management</h3>
<p className="mb-3">(a) To access Movie Flow, you are required to sign up for a Movie Flow account ("Account") and become a user ("User") of the Services. You can log in using the username and password you create, or through email verification. The first time you log in Movie Flow using E-mail verification, an Account of our platform will be provided for you.</p>
<p className="mb-3">(b) When creating an Account, you must:</p>
<ul className="list-disc list-inside ml-6 mb-3">
<li>Provide true, accurate, up-to-date, and complete information as we may from time to time request;</li>
<li>Not provide false or misleading information;</li>
<li>Not impersonate or attempt to impersonate another person;</li>
<li>Not create multiple accounts without our express consent;</li>
<li>Not use automated means to create accounts;</li>
<li>Safeguard your username and password and keep them confidential.</li>
</ul>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">2.3 Account Security and Responsibility</h3>
<p className="mb-3">(a) You are solely responsible for maintaining the security and confidentiality of your account login details and for all activities that occur under your Account, including any unauthorized use by third parties. If you do not take these precautions and your Account or password is lost, stolen, or misused, resulting in damage to you or others, you will be held legally responsible.</p>
<p className="mb-3">(b) You authorize us to assume that any person using Movie Flow with your username and password is either you or a person authorized to act for you.</p>
<p className="mb-3">(c) Your Account can only be used by yourself and you may not lend, give away, rent, transfer, sell, or share the Account to others. You will be responsible for any legal consequences arising from unauthorized use.</p>
<p className="mb-3">(d) You must notify us immediately of any suspected unauthorized use of your Account or breach of security.</p>
<p className="mb-3">(e) If you authorize any person to act for you in relation to the use of Movie Flow, you will ensure that they comply with this Agreement at all times.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">2.4 Account Verification</h3>
<p className="mb-3">We may require you to verify your Account through email verification, phone verification, or other means. Unverified accounts may have limited functionality or may be suspended.</p>
</div>
</section>
{/* Section 3: LICENSE AND ACCESS TO SERVICES */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">3. LICENSE AND ACCESS TO SERVICES</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">3.1 License</h3>
<p className="mb-3">Subject to your compliance with this Agreement, we grant you a non-exclusive, non-sublicensable, non-transferable, personal, limited license to use Movie Flow only on your personal smartphone, tablet, computer or other mobile or wireless device (which must be designated by us as being compatible for use with Movie Flow). We reserve all rights not expressly granted to you herein. We may terminate this license at any time, for any reason, with or without cause. This license does not grant you any rights to our intellectual property, trade secrets, or proprietary algorithms.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">3.2 Service Availability</h3>
<p className="mb-3">The Services are offered on an "as is" and "as available" basis at your sole risk. Our goal is to minimize disruption caused by technical errors; however, we cannot guarantee continuous, uninterrupted, or error-free operability of Movie Flow at all times. There may be times when certain functionality, features, content, or the entire Movie Flow becomes unavailable (whether on a scheduled or unscheduled basis), modified, suspended, or withdrawn by us, at our sole discretion, without notice to you. You agree that we will not be liable for any unavailability, modification, suspension, or withdrawal of Movie Flow or any part thereof.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">3.3 Technical Requirements</h3>
<p className="mb-3">You are responsible for providing the device, wireless service plan, software, Internet connections, and/or other equipment or services needed to access and use the Services. We do not guarantee that the Platform can be accessed on any particular device or with any particular service plan, or that it will be available in any particular geographic location. We are not responsible if you cannot access the Services properly or at all because of any event out of our control, for example (without limitation) the performance of any software or operating system running on your device or any connected software, hardware, network or service.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">3.4 Usage Limitations</h3>
<p className="mb-3">We may impose usage limitations including but not limited to: number of videos generated per time period, maximum video length, resolution limits, bandwidth restrictions, and storage limitations. These limitations may vary based on your subscription plan.</p>
</div>
</section>
{/* Section 4: PROHIBITED CONDUCT AND USE RESTRICTIONS */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">4. PROHIBITED CONDUCT AND USE RESTRICTIONS</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">4.1 General Prohibited Activities</h3>
<p className="mb-3">You may not:</p>
<p className="mb-3">(a) Access or use the Services if you are not fully able and legally competent to agree to these Terms or lack proper authorization;</p>
<p className="mb-3">(b) Make unauthorized copies, modify, adapt, translate, delete, alter, reverse engineer, disassemble, decompile, or create derivative works of the Services or any content included therein, including any files, tables or documentation (or any portion thereof) or determine or attempt to determine any source code, algorithms, methods or techniques embodied by the Services or any derivative works thereof;</p>
<p className="mb-3">(c) Distribute, license, transfer, sell, market, rent, or lease the Services or use them for commercial solicitation without our written consent;</p>
<p className="mb-3">(d) Interfere with or disrupt the proper working of the Services, bypass security measures, use automated scripts to collect information, or attempt to gain unauthorized access to any part of our systems;</p>
<p className="mb-3">(e) Incorporate the Services into other programs or products, except as expressly permitted;</p>
<p className="mb-3">(f) Use the Services to upload, transmit, or distribute malicious code, viruses, or other harmful materials;</p>
<p className="mb-3">(g) Impersonate any person or entity or create false identities, or falsely state or otherwise misrepresent you or your affiliation with any person or entity;</p>
<p className="mb-3">(h) Use the Services in ways that violate intellectual property rights or other rights of third parties;</p>
<p className="mb-3">(i) Circumvent, remove, alter, deactivate, degrade or thwart any technological measure implemented by us or any of our providers;</p>
<p className="mb-3">(j) Use the Services for any unlawful purpose or in violation of applicable laws and regulations;</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">4.2 Content Restrictions</h3>
<p className="mb-3">You may not use the Services to create, upload, transmit, distribute, store or otherwise make available content that:</p>
<p className="mb-3">(a) Is defamatory, obscene, offensive, pornographic, hateful, inflammatory, or discriminatory;</p>
<p className="mb-3">(b) Constitutes, encourages, or provides instructions for criminal offenses, dangerous activities, or self-harm;</p>
<p className="mb-3">(c) Is designed to harass, harm, intimidate, threaten, or upset others, including trolling and bullying;</p>
<p className="mb-3">(d) Contains threats of any kind, including threats of physical violence;</p>
<p className="mb-3">(e) Violates any applicable laws or regulations;</p>
<p className="mb-3">(f) Infringes on copyright, trademark, or other intellectual property rights;</p>
<p className="mb-3">(g) Contains personally identifiable information that could be used to harm individuals, including addresses, phone numbers, email addresses, number and feature in the personal identity document (e.g., national identification numbers, passport numbers) or credit card numbers;</p>
<p className="mb-3">(h) Is deliberately false or misleading with intent to harm others;</p>
<p className="mb-3">(i) Depicts non-consensual intimate imagery or facilitates non-consensual sharing of intimate content;</p>
<p className="mb-3">(j) Promotes or facilitates illegal activities including but not limited to drug use, violence, or fraud;</p>
<p className="mb-3">(k) Contains content that sexualizes, grooms, abuses, or otherwise harms children;</p>
<p className="mb-3">(l) Is racist or discriminatory, including discrimination on the basis of someone's race, religion, age, gender, disability or sexuality;</p>
<p className="mb-3">(m) Contains any unsolicited or unauthorized advertising, solicitations, promotional materials, or any other prohibited form of solicitation.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">4.3 AI-Specific Restrictions</h3>
<p className="mb-3">(a) You may not use the Services or any Content for purposes of creating, testing, improving, training, or developing competing artificial intelligence or machine learning models, systems, or technology without our written consent.</p>
<p className="mb-3">(b) You understand that artificial intelligence and machine learning are rapidly evolving fields of study. We are constantly working to improve our Services to make them more accurate, reliable, safe, and beneficial. Given the probabilistic nature of machine learning, use of our Services may, in some situations, result in Output that does not accurately reflect real people, places, or facts. You should not rely on Output as your only source of truth or factual information, or as a substitute for professional advice.</p>
<p className="mb-3">(c) You are responsible for evaluating the accuracy and appropriateness of the Output for your specific use case, which may include human review, before using or sharing any Output generated by our Services.</p>
<p className="mb-3">(d) You must not use any Output related to an individual for purposes that could have a legal or material impact on that person, such as decisions regarding credit, education, employment, housing, insurance, legal matters, medical issues, or other important areas.</p>
<p className="mb-3">(e) You may not use the Services to create deepfakes or other synthetic media designed to deceive or mislead viewers about the authenticity of the content.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">4.4 Platform Integrity</h3>
<p className="mb-3">(a) You must not attempt to manipulate the Services through unauthorized automation, spam, or other deceptive practices;</p>
<p className="mb-3">(b) You must not engage in activities that could damage, disable, overburden, or impair our servers or networks;</p>
<p className="mb-3">(c) You must not attempt to gain unauthorized access to any part of the Services, accounts, computer systems, or networks connected to the Services.</p>
</div>
</section>
{/* Section 5: INTELLECTUAL PROPERTY AND CONTENT */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">5. INTELLECTUAL PROPERTY AND CONTENT</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">5.1 Respect for Intellectual Property</h3>
<p className="mb-3">We respect intellectual property rights and encourage you to upload or transmit original content, and we will take measures to protect your intellectual property rights in accordance with applicable laws and regulations. You agree not to use the Services to infringe on any intellectual property rights. We reserve the right to block access or terminate accounts of users who infringe or allegedly infringe intellectual property rights. We will respond to valid takedown notices in accordance with applicable law.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">5.2 Input and Output</h3>
<p className="mb-3">(a) You may upload, post, or transmit content, data, or information through the Services, including but not limited to text, words, graphics, software, photos, and other images, trademarks, logos, videos, audio, musical or non-musical works, live performances, etc. (collectively, "Input") and receive content generated in response to your Input ("Output"). Input and Output are collectively referred to as "Content."</p>
<p className="mb-3">(b) You are responsible for your Content, including ensuring it does not violate applicable laws or this Agreement.</p>
<p className="mb-3">(c) You warrant and undertake that you hold intellectual property rights to the Input or have obtained legal authorization from the relevant owner to the use of the Input and that your use of the Input shall not violate any applicable laws and regulations or infringe upon the legitimate rights and interests of any third parties (including, without limitation, copyright, patent, trademark and other intellectual property rights and personality rights, personal data rights and other rights and interests). We reserve the right to moderate, block, or delete the Input (or part thereof) upon notification from an intellectual property rights owner or any other persons of any suspected or actual intellectual property rights infringement, and you shall be liable for any and all loss, damages or other consequences incurred by us or our affiliates arising out of, in connection with and/or relating to such suspected or actual infringement.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">5.3 Content Ownership and Licensing</h3>
<p className="mb-3">(a) To the extent permitted by applicable laws, the intellectual property and other property interests (if any) related to the Output generated in the process of the Services shall be vested in the owner thereof as defined by applicable laws. As between you and us, if you comply with these Terms, you own the Input you upload and the Output generated in response (together, "Assets"), subject to applicable laws and the underlying rights in any third-party content.</p>
<p className="mb-3">(b) You acknowledge and agree that if you hold intellectual property rights, portrait rights or other legitimate rights or interests on the Input and/or Output in accordance with applicable laws, your use of the Services will not constitute a transfer of such legitimate rights or interests, unless agreed otherwise by you and us. Specifically, without our written permission, you may not use, reproduce, distribute, and create derivative works of, and make modifications to, the Output for any commercial purposes.</p>
<p className="mb-3">(c) Subject to legitimate commercial purposes in accordance with applicable laws, you agree that Movie Flow has a non-exclusive, royalty-free right and license during the operational period of its business and within the territories in which Movie Flow conducts business to use the Content, which shall comprise the Input you provide or upload to the Services and Output generated during your use of our Services, including such right to host, store, transfer, publicly display, publicly perform (including by means of a digital audio transmission), communicate to the public, reproduce, modify for the purpose of formatting for display, create derivative works from, and to distribute Input and Output, in whole or in part, in any media formats and through any media channels.</p>
<p className="mb-3">This license survives termination of our agreement with you.</p>
<p className="mb-3">(d) "Use" means to reproduce, modify, adapt, prepare derivative works of, communicate to the public, publicly perform or display, distribute, and otherwise use or exploit.</p>
<p className="mb-3">(e) To the extent permitted by law, you waive any moral rights or similar rights in respect to your Assets.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">5.4 Company Use of Content</h3>
<p className="mb-3">We may, at our discretion or by licensing to third parties, use or develop the aforementioned Content (in whole or in part) for the purposes of promotion, product/function upgrades, and to research on new products/functions. You undertake not to claim personal rights or property rights in connection with our use or development of the Content.</p>
<p className="mb-3">Without limiting the generality of the foregoing license, Movie Flow may process usage data, aggregated data, or Input for its lawful business purposes, in accordance with applicable laws including but not limited to the following:</p>
<ul className="list-disc list-inside ml-6 mb-3">
<li>Track use of the Services for billing purposes;</li>
<li>Provide support for the Services;</li>
<li>Monitor the performance and stability of the Services;</li>
<li>Prevent or address technical issues with the Services;</li>
<li>Improve the Services, its other products and services, and to develop new products and services;</li>
<li>Create, test, improve, train, or otherwise develop the artificial intelligence or machine learning models, systems, architecture, weights or related technology used by Movie Flow AI in connection with the Services;</li>
<li>For all other lawful business practices, such as analytics, benchmarking, and reports.</li>
</ul>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">5.5 Content Removal and Moderation</h3>
<p className="mb-3">(a) We reserve the right to moderate, block, or delete Content that we believe violates this Agreement or is otherwise objectionable, with or without notice. We may use automated systems and human reviewers for content moderation. In addition to complying with the content removal orders from any relevant judicial or administrative authorities or requests of the Users or any third parties, in response to complaints from other Users or any third parties, or if we, in our sole discretion, believe that any Content does not comply with this Agreement, we may take any measures, including without limitation removing or refusing to display such Content.</p>
<p className="mb-3">(b) You may revoke authorization for us to use Content by notifying us at support@movieflow.ai, subject to certain limitations including Content already incorporated into AI model training.</p>
<p className="mb-3">(c) We prioritize reports related to child safety and will take immediate action on such content.</p>
<p className="mb-3">(d) You acknowledge and agree that Content you uploaded or published will be considered non-proprietary and non-confidential. You must not upload or transit any Content that you consider to be proprietary or confidential. You may decide and control the extent of the availability of the Content (i.e. whether to all other users, users you selected or only to yourself) by way of Movie Flow's settings, subject to features and functions available in Movie Flow.</p>
<p className="mb-3">(e) Notwithstanding the foregoing, the whole or part of Content uploaded or published by one User may be extracted by another User to produce additional Content, subject to prior approval from the User, where applicable.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">5.6 Brand Attribution</h3>
<p className="mb-3">You acknowledge that, without our written permission, when you use, distribute or disseminate the AI function to generate content, you shall label the Output interface with the brand "Movie Flow" and logo involved brands or logos centered on or derived from the aforementioned brands or logos, and combination of the aforementioned brands or logos with others. In cases that the Output generated by Movie Flow are not labelled with any brand mark in the process of use due to objective reasons, you shall prominently indicate that the Output is generated by "Movie Flow" in the use scenarios of the generated content (including but not limited to adding the aforesaid brand logo with "Movie Flow" or other relevant brand or logo in the interface of the generated content or marking in the title or other prominent positions).</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">5.7 Company Intellectual Property</h3>
<p className="mb-3">The Services, including all software, algorithms, designs, text, graphics, images, videos, information, logos, button icons, and other materials, are owned by Company or its licensors and are protected by copyright, trademark, and other intellectual property laws. You acknowledge that Movie Flow and the Content provided through Movie Flow are subject to protection by trademark, copyright and other intellectual property rights. You may not use Content from our Services unless you obtain permission from its owner or are otherwise permitted by applicable laws. Do not remove, obscure, or alter any legal notices displayed in or along with our Services.</p>
</div>
</section>
{/* Section 6: COPYRIGHT COMPLAINTS */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">6. COPYRIGHT COMPLAINTS</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">6.1 Copyright Infringement Claims</h3>
<p className="mb-3">If you believe your intellectual property rights have been infringed, please send notice in accordance with the Digital Millennium Copyright Act (DMCA) or applicable laws to: support@movieflow.ai</p>
<p className="mb-3">We may delete or disable Content that we believe violates this Agreement or is alleged to be infringing and will terminate accounts of repeat infringers where appropriate. Written claims must include:</p>
<p className="mb-3">(a) A physical or electronic signature of the person authorized to act on behalf of the copyright owner; (b) A description of the copyrighted work claimed to have been infringed; (c) A description of where the allegedly infringing material is located on our website and/or APP so we can find it; (d) Your contact information; (e) A statement of good-faith belief that the use is not authorized by the copyright owner, its agent, or the law; (f) A statement that the information is accurate and, under penalty of perjury, that you are the copyright owner or authorized to act on behalf of the copyright owner.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">6.2 Counter-Notice</h3>
<p className="mb-3">If you receive notification that your Content is subject to an infringement claim, you may submit a written counter-notice to us at support@movieflow.ai in accordance with DMCA or applicable law requirements.</p>
</div>
</section>
{/* Section 7: COMMERCIAL TERMS */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">7. COMMERCIAL TERMS</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">7.1 Service Fees</h3>
<p className="mb-3">The Services we provide may be free or paid services. Specific details are subject to information displayed on Movie Flow. We may adjust fee standards and methods based on business development needs and may start charging for previously free services. We will notify users of such changes through Movie Flow with reasonable advance notice. For clarity, the provision of free services through Movie Flow shall not be construed as a waiver of our right to charge fees in the future.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">7.2 Billing and Payment</h3>
<p className="mb-3">(a) For paid services, you agree to pay all applicable fees as specified in your chosen plan; (b) Payments are non-refundable except as required by applicable law or as expressly stated in these Terms; (c) We may change our pricing with 30 days' notice for new subscriptions and at the end of your current billing cycle for existing subscriptions; (d) You authorize us to charge your designated payment method for all applicable fees.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">7.3 Third-Party Services</h3>
<p className="mb-3">Movie Flow may contain services provided by affiliated companies or third parties. We provide access points for these services for your convenience. If you use such services, you should enter into separate agreements with the corresponding service providers and bear all potential risks. We provide no guarantee or warranty for third-party services.</p>
</div>
</section>
{/* Section 8: DISCLAIMERS AND LIMITATION OF LIABILITY */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">8. DISCLAIMERS AND LIMITATION OF LIABILITY</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">8.1 Disclaimer of Warranties</h3>
<p className="mb-3">(a) Your use of the Services is entirely at your own risk. By using the Services, you confirm that you have carefully considered terms and conditions under this Agreement and fully understand its implications on your legal rights.</p>
<p className="mb-3">(b) THE SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITH ALL FAULTS. WE MAKE NO WARRANTY OR REPRESENTATION WITH RESPECT TO THE SERVICES.</p>
<p className="mb-3">(c) WE DO NOT REPRESENT OR WARRANT THAT:</p>
<ul className="list-disc list-inside ml-6 mb-3">
<li>YOUR USE OF THE SERVICES WILL MEET YOUR REQUIREMENTS OR ACHIEVE INTENDED RESULTS;</li>
<li>THE SERVICES WILL BE UNINTERRUPTED, TIMELY, SECURE, OR ERROR-FREE;</li>
<li>ANY INFORMATION OBTAINED WILL BE ACCURATE, COMPLETE, UP-TO-DATE, OR RELIABLE;</li>
<li>DEFECTS WILL BE CORRECTED;</li>
<li>THE SERVICES ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS.</li>
</ul>
<p className="mb-3">(d) TO THE FULLEST EXTENT PROVIDED BY LAW, WE DISCLAIM ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND FITNESS FOR PARTICULAR PURPOSE.</p>
<p className="mb-3">(e) NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM US SHALL CREATE ANY WARRANTY NOT EXPRESSLY STATED IN THESE TERMS.</p>
<p className="mb-3">(f) We cannot and do not guarantee that any Content will be free from viruses and/or other code that may have contaminating or destructive elements. It is your responsibility to implement appropriate information technology security safeguards (including anti-virus and other security checks) to satisfy your particular requirements as to the safety and reliability of Movie Flow and the content made available through them.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">8.2 Limitation of Liability</h3>
<p className="mb-3">(a) NOTHING IN THIS AGREEMENT SHALL EXCLUDE OR LIMIT OUR LIABILITY FOR LOSSES WHICH MAY NOT BE LAWFULLY EXCLUDED OR LIMITED BY APPLICABLE LAWS, INCLUDING LIABILITY FOR DEATH OR PERSONAL INJURY CAUSED BY NEGLIGENCE AND FOR FRAUD.</p>
<p className="mb-3">(b) You agree to defend, indemnify, and hold harmless us, our parent companies, subsidiaries, and affiliates and each of our respective officers, directors, employees, agents and advisors from any and all claims, liabilities, costs, and expenses, including, but not limited to, attorneys' fees and expenses on an indemnity basis, arising out of a breach by you, your content or any user of your Account of applicable laws and regulations or this Agreement, including your obligations, representation and warranties herein.</p>
<p className="mb-3">(c) SUBJECT TO THE ABOVE, WE SHALL NOT BE LIABLE FOR:</p>
<ul className="list-disc list-inside ml-6 mb-3">
<li>ANY LOSS OF PROFIT, GOODWILL, OR OPPORTUNITY;</li>
<li>ANY LOSS, CORRUPTION, OR MISUSE OF DATA;</li>
<li>ANY INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES;</li>
<li>ANY DAMAGES ARISING FROM THE USE OR MISUSE OF AI-GENERATED CONTENT;</li>
<li>ANY DAMAGES ARISING FROM THIRD-PARTY ACTIONS OR CONTENT;</li>
<li>ANY INTERRUPTION, INTERCEPTION, SUSPENSION, DELAY, LOSS, UNAVAILABILITY, OR OTHER FAILURE IN PROVIDING THE SERVICES OR YOUR USE OF MOVIE FLOW, IN TRANSMITTING INSTRUCTIONS OR INFORMATION RELATING TO THE SERVICES CAUSED BY ANY ACTS, OMISSIONS OR CIRCUMSTANCES BEYOND OUR REASONABLE CONTROL.</li>
</ul>
<p className="mb-3">(d) OUR TOTAL LIABILITY TO YOU, WHETHER IN CONTRACT, TORT, OR OTHERWISE, SHALL BE LIMITED TO THE TOTAL AMOUNT OF PAYMENTS MADE BY YOU TO US IN THE PAST 6 MONTHS OR $50, WHICHEVER IS GREATER.</p>
</div>
</section>
{/* Section 9: PRIVACY AND PERSONAL DATA */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">9. PRIVACY AND PERSONAL DATA</h2>
<p className="mb-3">We collect and process personal data of users in accordance with our Privacy Policy, which is incorporated into this Agreement by reference. All personal data handling complies with applicable privacy laws and regulations, including GDPR, CCPA, and other applicable data protection laws.</p>
</section>
{/* Section 10: THIRD-PARTY PROVIDERS AND EXTERNAL LINKS */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">10. THIRD-PARTY PROVIDERS AND EXTERNAL LINKS</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">10.1 Third-Party Content and Services</h3>
<p className="mb-3">You acknowledge that certain parts of Movie Flow and content made available through the Services may be provided by third parties. We are not responsible for examining or evaluating third-party content, accuracy, availability, or quality. We bear no responsibility for third-party content or services. Subject to our Privacy Policy, we may explore and integrate developer tools provided by third parties from time to time to enable or facilitate features, functions or business for Movie Flow platform in accordance with this Agreement.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">10.2 External Links</h3>
<p className="mb-3">Movie Flow may contain links to other websites or resources. Although these websites or resources are selected with care, we are not responsible for the content, accuracy, or privacy practices of external sites. The inclusion of links does not imply endorsement. Your use of external websites is subject to their respective terms and conditions.</p>
</div>
</section>
{/* Section 11: TERM AND TERMINATION */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">11. TERM AND TERMINATION</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">11.1 Term</h3>
<p className="mb-3">This Agreement starts on the Effective Date and continues until terminated by either party in accordance with this Section.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">11.2 Termination by User</h3>
<p className="mb-3">You may stop using our Services at any time and delete your Account through account settings or by contacting us at support@movieflow.ai. Once you delete your Account, you cannot reactivate it or recover associated content, unless required by applicable law.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">11.3 Termination by Company</h3>
<p className="mb-3">We may terminate this Agreement or suspend, block, or delete your Account at any time for any reason, with or without notice, including if:</p>
<ul className="list-disc list-inside ml-6 mb-3">
<li>You violate these Terms or applicable laws;</li>
<li>Your use could cause risk or harm to us, other users, or others;</li>
<li>You do not comply with applicable laws;</li>
<li>Your Account has been inactive for an extended period;</li>
<li>We are required to do so by law or government request.</li>
</ul>
<p className="mb-3">If you violate or are suspected of violating applicable laws and regulations or this Agreement, we reserve the right to take all necessary actions (including but not limited to suspending, blocking or deleting your Account or your use of the Services, or reporting to the relevant authorities) immediately without notice to you at our sole discretion.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">11.4 Effect of Termination</h3>
<p className="mb-3">Upon termination, provisions that by their nature should survive will survive, including liability obligations, indemnities, and intellectual property licenses granted to us. We may delete your Content and Account data, subject to applicable law and our Privacy Policy.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">11.5 Appeals</h3>
<p className="mb-3">If you believe we have suspended or terminated your Account in error, you may file an appeal by contacting our support team at support@movieflow.ai within 30 days of the termination.</p>
</div>
</section>
{/* Section 12: DISPUTE RESOLUTION */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">12. DISPUTE RESOLUTION</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">12.1 Negotiation</h3>
<p className="mb-3">Any dispute, controversy or claim (whether in contract, tort or otherwise) arising out of, relating to, or in connection with this Agreement, including their existence, validity, interpretation, performance, breach or termination shall first be settled through friendly and amicable negotiation between you and us for a period of 30 days.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">12.2 Arbitration</h3>
<p className="mb-3">If negotiation fails, disputes will be referred to and finally resolved by arbitration administered by the Hong Kong International Arbitration Centre (HKIAC) in accordance with the arbitration rules of the HKIAC then in force when the Notice of Arbitration is submitted by a party. The seat of arbitration will be Hong Kong, and proceedings will be conducted in English. The arbitrator shall be selected in accordance with HKIAC rules.</p>
<p className="mb-3">The arbitration will be conducted by videoconference if possible, but if the arbitrator determines a hearing should be conducted in person, the venue of the hearing will be mutually agreed upon, failing which the venue of the hearing shall be determined by the sole arbitrator. You and the Company agree that any settlement amount offered by any party will not be disclosed to the arbitrator by either party until after the arbitrator determines the final award, if any.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">12.3 Individual Disputes Only</h3>
<p className="mb-3">You and the Company agree that disputes must be resolved on an individual basis only, and cannot be pursued as a plaintiff or class member in any alleged class, consolidated, or representative actions. Class arbitrations, class actions, and representative actions are prohibited. Only individual relief is available. The parties agree to separate and litigate in court any requests for public injunctive relief after completing arbitration for the underlying claim and all other claims. The foregoing does not prevent either party from participating in a class-wide settlement. Additionally, you and the Company knowingly and irrevocably waive any right to trial by jury in any action, proceeding, or counterclaim.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">12.4 Jury Trial Waiver</h3>
<p className="mb-3">BY ACCEPTING THIS AGREEMENT, YOU AND MOVIE FLOW WAIVE THE RIGHT TO A TRIAL BY JURY OR TO PARTICIPATE IN ANY CLASS ACTION OR REPRESENTATIVE PROCEEDING.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">12.5 Emergency Relief</h3>
<p className="mb-3">Notwithstanding the arbitration provision, either party may seek emergency equitable relief before a competent court to prevent irreparable harm.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">12.6 Severability of Dispute Resolution</h3>
<p className="mb-3">If any part of terms of this Section 12 is found to be illegal or unenforceable, the remainder will remain in effect, except that if a finding of partial illegality or unenforceability would allow class arbitration, class action, or representative action, this entire dispute resolution section will be unenforceable in its entirety.</p>
</div>
</section>
{/* Section 13: GENERAL PROVISIONS */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">13. GENERAL PROVISIONS</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">13.1 Governing Law</h3>
<p className="mb-3">To the fullest extent permitted by applicable laws and regulations, this Agreement shall be governed by and construed in accordance with the laws of Hong Kong, without regard to conflict of law principles, provided that nothing shall prevent us from bringing proceedings to protect our intellectual property rights before any competent court in any other jurisdiction.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">13.2 Entire Agreement</h3>
<p className="mb-3">This Agreement constitutes the entire agreement between you and us regarding your use of the Services and supersedes all prior agreements, except for any separate written agreements between the parties.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">13.3 Severability</h3>
<p className="mb-3">If any term of this Agreement is held by a court or tribunal to be invalid, illegal or unenforceable and can be deleted without altering the essence of this Agreement, it shall be deemed deleted without affecting the validity and enforceability of the remaining terms of this Agreement. If the invalid, illegal or unenforceable provision cannot be deleted without altering the essence of this Agreement, we may amend this Agreement to remedy such invalidity, illegality or unenforceability to the extent needed to achieve the intent of the original provision.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">13.4 Assignment</h3>
<p className="mb-3">(a) You shall not without our prior written consent, assign, transfer, novate, part with, outsource, subcontract or delegate any of your rights, responsibilities and/or obligations under this Agreement (in whole or in part); and</p>
<p className="mb-3">(b) We may assign, transfer, novate, part with or subcontract any of our rights, responsibilities and/or obligations under this Agreement (in whole or in part) to any other affiliate, subsidiary, or successor in interest of any business associated with our Services without your prior consent.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">13.5 Waiver</h3>
<p className="mb-3">The failure by either party to exercise or enforce any right or remedy of this Agreement will not constitute a waiver of such right or remedy, nor shall it prevent or restrict the further exercise of that or any other right or remedy. No single or partial exercise of such right or remedy shall prevent or restrict the further exercise of that or any other right or remedy. Any waiver of any term of this Agreement will be effective only if in writing and signed by the relevant party.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">13.6 Feedback</h3>
<p className="mb-3">We welcome feedback, comments and suggestions for improvements to the Services ("Feedback"). Feedback is provided on a non-confidential basis, and we have no obligation to maintain its confidentiality. You acknowledge and expressly agree that any contribution of Feedback does not and will not give or grant you any right, title or interest in the Services or in any such Feedback. The Company may use and disclose Feedback in any manner and for any purpose whatsoever without further notice or compensation to you and without retention by you of any proprietary or other right or claim. You hereby assign to the Company any and all right, title and interest (including, but not limited to, any patent, copyright, trade secret, trademark, show-how, know-how, moral rights and any and all other intellectual property right) that you may have in and to any and all Feedback.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">13.7 Language</h3>
<p className="mb-3">This Agreement is drafted in English. If translated into other languages, the English version shall prevail in case of inconsistency, unless otherwise required under applicable laws.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">13.8 Force Majeure</h3>
<p className="mb-3">We shall not be liable for any failure or delay in performance under this Agreement which is due to fire, flood, earthquake, elements of nature, acts of God, acts of war, terrorism, riots, civil disorders, rebellions, pandemic, or other cause beyond our reasonable control.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">13.9 Export Control</h3>
<p className="mb-3">You agree to comply with all applicable export control laws and regulations, and you shall not export, re-export, or transfer the Services to prohibited countries or persons.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">13.10 Additional Provisions</h3>
<p className="mb-3">(a) Each party shall pay its own costs incurred in connection with the performance of any of its obligations under this Agreement, save to the extent that is expressly provided otherwise in this Agreement.</p>
<p className="mb-3">(b) No provision in this Agreement is intended to create or shall create a partnership between the parties or establishes a party as the agent of another party for any purpose. A party has no authority to act for, bind, contract in the name of, or create a liability for the other party by any means or for any purpose.</p>
<p className="mb-3">(c) Each party shall, and shall use all reasonable endeavors to procure that any necessary third party shall, execute and deliver such documents and perform such acts as may reasonably be required for the purpose of giving full effect to this Agreement.</p>
<p className="mb-3">(d) A person who is not a party to this Agreement has no right to enforce any terms of this Agreement.</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">13.11 Survival</h3>
<p className="mb-3">Upon termination or expiration of this Agreement, any provision which, by its nature or express terms should survive, including but not limited to any obligations in relation to the liability of, or indemnities (if any) given by, the respective parties, will survive such termination or expiration.</p>
</div>
</section>
{/* Section 14: CONTACT INFORMATION */}
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">14. CONTACT INFORMATION</h2>
<p className="mb-3">If you have any questions, complaints, or suggestions regarding this Agreement, please contact us:</p>
<p className="mb-3">(a) Through email at support@movieflow.ai; (b) By accessing [Community] in the top navigation bar of the Website.</p>
<p className="mb-3">We will review issues and reply to you in a timely manner after verifying your identity.</p>
</section>
{/* Final Statement */}
<section className="mb-8">
<p className="text-lg font-semibold text-center">By using Movie Flow, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service.</p>
</section>
</div>
</div>
</div>
);
}

108
app/activate/page.tsx Normal file
View File

@ -0,0 +1,108 @@
"use client";
import { post } from "@/api/request";
import { useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react";
import { CheckCircle, XCircle, Loader2 } from "lucide-react";
export default function Activate() {
const searchParams = useSearchParams();
const t = searchParams.get("t") || '';
const type = searchParams.get("type");
if (type === "confirm_email") {
return <ConfirmEmail t={t} />;
}
return <></>;
}
/**
* Email verification confirmation component
* @param {string} t - Verification token
*/
function ConfirmEmail({ t }: { t: string }) {
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [message, setMessage] = useState('');
useEffect(() => {
if (!t) {
setStatus('error');
setMessage('Invalid verification token');
return;
}
post(`${process.env.NEXT_PUBLIC_JAVA_URL}/api/user/activate?t=${t}`)
.then((res) => {
setStatus('success');
setMessage('Your registration has been verified. Please return to the official website to log in.');
})
.catch((err) => {
setStatus('error');
setMessage('Verification failed. Please try again.');
});
}, [t]);
const renderContent = () => {
switch (status) {
case 'loading':
return (
<div data-alt="loading-state" className="flex flex-col items-center justify-center space-y-4">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-cyan-400 to-purple-600 rounded-full blur-lg opacity-30 animate-pulse"></div>
<Loader2 className="h-16 w-16 animate-spin text-cyan-400 relative z-10" />
</div>
<p className="text-lg text-gray-300">Verifying email...</p>
</div>
);
case 'success':
return (
<div data-alt="success-state" className="flex flex-col items-center justify-center space-y-4">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-cyan-400 to-purple-600 rounded-full blur-lg opacity-30"></div>
<CheckCircle className="h-16 w-16 text-cyan-400 relative z-10" />
</div>
<h2 className="text-2xl font-semibold bg-gradient-to-r from-cyan-400 to-purple-600 bg-clip-text text-transparent">Verification Successful</h2>
<p className="text-gray-300 text-center max-w-md">{message}</p>
</div>
);
case 'error':
return (
<div data-alt="error-state" className="flex flex-col items-center justify-center space-y-4">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-red-500 to-orange-500 rounded-full blur-lg opacity-30"></div>
<XCircle className="h-16 w-16 text-red-400 relative z-10" />
</div>
<h2 className="text-2xl font-semibold bg-gradient-to-r from-red-500 to-orange-500 bg-clip-text text-transparent">Verification Failed</h2>
<p className="text-gray-300 text-center max-w-md">{message}</p>
<button
data-alt="retry-button"
onClick={() => window.location.reload()}
className="mt-4 px-6 py-2 bg-gradient-to-r from-cyan-400 to-purple-600 text-white rounded-lg hover:from-cyan-500 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl transform hover:scale-105"
>
Retry Verification
</button>
</div>
);
}
};
return (
<div data-alt="email-verification-container" className="min-h-screen flex items-center justify-center bg-gradient-to-br from-black via-gray-900 to-black px-4">
<div data-alt="verification-card" className="bg-gradient-to-br from-gray-900/90 to-black/90 backdrop-blur-sm rounded-2xl shadow-2xl p-8 max-w-md w-full border border-gray-700/50 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-cyan-400/10 to-purple-600/10 pointer-events-none"></div>
<div className="relative z-10">
{status === 'loading' && (
<div data-alt="verification-header" className="text-center mb-8">
<h1 className="text-3xl font-bold text-white mb-2 bg-gradient-to-r from-cyan-400 to-purple-600 bg-clip-text text-transparent">Email Verification</h1>
<p className="text-gray-300">Please wait while we verify your email</p>
</div>
)}
{renderContent()}
</div>
</div>
</div>
);
}

View File

@ -1,7 +0,0 @@
'use client';
import WorkFlow from '@/components/pages/work-flow';
export default function ScriptWorkFlowPage() {
return <WorkFlow />;
}

View File

@ -26,7 +26,6 @@ export default function RootLayout({
}) {
const [showCallbackModal, setShowCallbackModal] = useState(false)
const openCallback = async function (ev: MessageEvent<any>) {
console.log(ev)
if (ev.data.type === 'waiting-payment') {
setShowCallbackModal(true)
}
@ -40,14 +39,25 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<head>
<title>Movie Flow - Create Amazing Movies with AI</title>
<meta name="description" content="Professional AI-powered video creation platform with advanced editing tools" />
<title>MovieFlow - AI Film Studio</title>
<meta name="description" content="Share your story, or just a few words, and our AI turns it into a great film. We remove the barriers to creation. At MovieFlow, everyone is a movie master." />
<meta name="robots" content="noindex" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" sizes="16x16" href="/favicon.ico?v=1" />
<link rel="icon" type="image/x-icon" sizes="32x32" href="/favicon.ico?v=1" />
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=1" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.ico?v=1" />
<script async src="https://www.googletagmanager.com/gtag/js?id=G-E6VBGZ4ER5"></script>
<script
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){window.dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-E6VBGZ4ER5');
`,
}}
/>
</head>
<body className="font-sans antialiased">
<ConfigProvider
@ -60,6 +70,13 @@ export default function RootLayout({
colorBgMask: 'rgba(0, 0, 0, 0.6)',
borderRadius: 16,
},
components: {
Message: {
colorBgElevated: '#1f1f1f', // 自定义消息背景色
colorText: '#ffffff', // 自定义文字颜色
borderRadius: 8, // 自定义圆角
},
},
}}
>
<CallbackModalContext.Provider value={{ setShowCallbackModal }}>

View File

@ -0,0 +1,11 @@
'use client';
import dynamic from 'next/dynamic';
const WorkFlow = dynamic(() => import('@/components/pages/work-flow'), {
ssr: false
});
export default function ScriptWorkFlowPage() {
return <WorkFlow />;
}

View File

@ -1,13 +1,14 @@
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { TopBar } from "@/components/layout/top-bar";
import { HomePage2 } from "@/components/pages/home-page2";
import OAuthCallbackHandler from "@/components/ui/oauth-callback-handler";
export default function Home() {
return (
<DashboardLayout>
{/* Handle OAuth callbacks */}
<>
<TopBar collapsed={true} />
<OAuthCallbackHandler />
<HomePage2 />
</DashboardLayout>
</>
);
}

View File

@ -12,14 +12,48 @@ import {
CardTitle,
} from "@/components/ui/card";
import { fetchSubscriptionPlans, SubscriptionPlan } from "@/lib/stripe";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
export default function PricingPage() {
useEffect(() => {
// 获取当前窗口尺寸
const currentWidth = window.innerWidth;
const currentHeight = window.innerHeight;
// 计算缩放比例 (1920x1080)
const wScale = currentWidth / 1920;
const hScale = currentHeight / 1080;
// 检查app节点是否存在
const pricingPage = document.getElementById("pricing-page");
if (!pricingPage) {
console.error("未找到app节点");
return;
}
// setHPading((hScale || 1) * 10);
// 创建样式元素
const style = document.createElement("style");
// 设置CSS样式
style.textContent = `
#pricing-page {
transform-origin: top left;
transform: scale(${wScale}, ${hScale});
width: 1920px;
height: 1080px;
}
`;
// 将样式添加到head
document.head.appendChild(style);
}, []);
return (
<div className="w-full h-full overflow-y-auto bg-black text-white pb-[10rem]">
<DashboardLayout>
<div className="w-full h-full overflow-y-auto bg-black text-white pb-[10rem]" id="pricing-page">
{/* Main Content */}
<HomeModule5 />
</div>
</DashboardLayout>
);
}
/**价格方案 */
@ -49,6 +83,7 @@ function HomeModule5() {
credits: string;
buttonText: string;
features: string[];
issubscribed: boolean;
}[]
>(() => {
return plans.map((plan) => {
@ -60,15 +95,13 @@ function HomeModule5() {
: plan.price_year / 100,
credits: plan.description,
buttonText: plan.is_free ? "Try For Free" : "Subscribe Now",
issubscribed: plan.is_subscribed,
features: plan.features || [],
};
});
}, [plans, billingType]);
const handleSubscribe = async (planName: string) => {
if (planName === "hobby") {
return;
}
try {
// 使用新的Checkout Session方案更简单
@ -103,14 +136,14 @@ function HomeModule5() {
return (
<div
data-alt="core-value-section"
className="home-module5 h-[1600px] relative flex flex-col items-center justify-center w-full bg-black snap-start"
className="home-module5 h-[1300px] relative flex flex-col items-center justify-center w-full bg-black snap-start"
>
<div
data-alt="core-value-content"
className="center z-10 flex flex-col items-center mb-[8rem]"
className="center z-10 flex flex-col items-center mb-[4rem]"
>
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal mb-[1.5rem]">
Start Creating
Pick a plan and make it yours
</h2>
{/* 计费切换 */}
@ -133,7 +166,7 @@ function HomeModule5() {
: "text-white hover:text-gray-300"
}`}
>
Yearly - <span className="text-[#FFCC6D]">10%</span>
Yearly - <span className="text-[#FFCC6D]">20%</span>
</button>
</div>
</div>
@ -143,7 +176,7 @@ function HomeModule5() {
{pricingPlans.map((plan, index) => (
<div
key={index}
className=" w-[26rem] h-[38.125rem] bg-black rounded-lg p-[1.5rem] border border-white/20"
className=" w-[24rem] h-[38.125rem] bg-black rounded-2xl p-[1.5rem] border border-white/20"
>
<h3 className="text-white text-2xl font-normal mb-[1rem]">
{plan.title}
@ -152,17 +185,26 @@ function HomeModule5() {
<span className="text-white text-[3.375rem] font-bold">
${plan.price}
</span>
<span className="text-white text-xs ml-[0.5rem]">/month</span>
<span className="text-white text-xs ml-[0.5rem]">/ {billingType === "month" ? "mo" : "year"}</span>
</div>
<p className="text-white text-[0.875rem] mb-[1rem]">
{plan.credits}
</p>
<button
onClick={() => handleSubscribe(plan.title)}
className="w-full bg-white text-black py-[0.75rem] rounded-full mb-[1rem] hover:bg-black hover:text-white transition-colors border border-white/20"
>
{plan.buttonText}
</button>
{plan.issubscribed ? (
<button
disabled
className="w-full bg-gray-400 text-gray-600 py-[0.75rem] rounded-full mb-[1rem] cursor-not-allowed border border-gray-300"
>
Already Owned
</button>
) : (
<button
onClick={() => handleSubscribe(plan.title)}
className="w-full bg-white text-black py-[0.75rem] rounded-full mb-[1rem] hover:bg-black hover:text-white transition-colors border border-white/20"
>
{plan.buttonText}
</button>
)}
<p className="w-full text-center text-white/60 text-[0.75rem] mb-[2rem]">
* Billed monthly until cancelled
</p>

View File

@ -194,7 +194,7 @@ export const useRoleServiceHook = (): UseRoleService => {
// 更新角色列表中的对应角色描述
setRoleList((prev) =>
prev.map((role) =>
prev?.map((role) =>
role.id === selectedRole?.id
? { ...role, generateText: newContent }
: role

View File

@ -8,29 +8,14 @@ import { generateTextToImage } from "@/api/movie_start";
import { MovieProjectService, MovieProjectMode } from "./MovieProjectService";
import { CreateMovieProjectV3Request } from "@/api/DTO/movie_start_dto";
/** 模板角色接口 */
interface TemplateRole {
/** 角色名 */
role_name: string;
/** 照片URL */
photo_url: string;
/** 声音URL */
voice_url: string;
}
interface UseTemplateStoryService {
/** 模板列表 */
templateStoryList: StoryTemplateEntity[];
/** 当前选中要使用的模板 */
selectedTemplate: StoryTemplateEntity | null;
/** 当前选中的活跃角色索引 */
activeRoleIndex: number;
/** 计算属性:当前活跃角色信息 */
activeRole: TemplateRole | null;
/** 加载状态 */
isLoading: boolean;
/** 获取模板列表函数 */
/** 获取模板列表函数 */
getTemplateStoryList: () => Promise<void>;
/**
* action
@ -48,21 +33,18 @@ interface UseTemplateStoryService {
) => Promise<string | undefined>;
/** 设置选中的模板 */
setSelectedTemplate: (template: StoryTemplateEntity | null) => void;
/** 设置活跃角色索引 */
setActiveRoleIndex: (index: number) => void;
/** 设置当前活跃角色的音频URL */
setActiveRoleAudio: (audioUrl: string) => void;
/**清空数据 */
/** 清空数据 */
clearData: () => void;
/** 上传人物头像并分析 */
AvatarAndAnalyzeFeatures: (imageUrl: string, roleName?: string) => Promise<void>;
AvatarAndAnalyzeFeatures: (imageUrl: string, roleName: string) => Promise<void>;
/** 更新指定角色的图片 */
updateRoleImage: (roleName: string, imageUrl: string) => void;
/** 更新变量字段值 */
updateFillableContentField: (fieldName: string, fieldValue: string) => void;
/** 带防抖的失焦处理函数 */
handleFieldBlur: (fieldName: string, fieldValue: string) => void;
/** 更新指定道具的图片 */
updateItemImage: (itemName: string, imageUrl: string) => void;
/** 带防抖的失焦处理函数 - 角色图片生成 */
handleRoleFieldBlur: (roleName: string, fieldValue: string) => void;
/** 带防抖的失焦处理函数 - 道具图片生成 */
handleItemFieldBlur: (itemName: string, fieldValue: string) => void;
}
export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
@ -71,26 +53,14 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
>([]);
const [selectedTemplate, setSelectedTemplate] =
useState<StoryTemplateEntity | null>(null);
const [activeRoleIndex, setActiveRoleIndex] = useState<number>(0);
const [isLoading, setIsLoading] = useState(false);
// 使用上传文件Hook
const { uploadFile } = useUploadFile();
/** 模板故事用例实例 */
const templateStoryUseCase = useMemo(() => new TemplateStoryUseCase(), []);
/** 计算属性:当前活跃角色信息 */
const activeRole = useMemo(() => {
if (
!selectedTemplate ||
activeRoleIndex < 0 ||
activeRoleIndex >= selectedTemplate.storyRole.length
) {
return null;
}
return selectedTemplate.storyRole[activeRoleIndex];
}, [selectedTemplate, activeRoleIndex]);
/**
*
*/
@ -102,8 +72,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
setTemplateStoryList(templates);
setSelectedTemplate(templates[0]);
setActiveRoleIndex(0);
console.log(selectedTemplate, activeRoleIndex);
console.log(selectedTemplate);
} catch (err) {
console.error("获取模板列表失败:", err);
} finally {
@ -111,119 +80,17 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
}
}, [templateStoryUseCase]);
/**
*
*/
const handleSetActiveRoleIndex = useCallback((index: number): void => {
setActiveRoleIndex(index);
}, []);
/**
* URL
*/
const setActiveRoleData = useCallback(
(imageUrl: string): void => {
if (
!selectedTemplate ||
activeRoleIndex < 0 ||
activeRoleIndex >= selectedTemplate.storyRole.length
) {
console.log(selectedTemplate, activeRoleIndex);
return;
}
try {
// const character_brief = {
// name: selectedTemplate.storyRole[activeRoleIndex].role_name,
// image_url: imageUrl,
// character_analysis: JSON.parse(desc).character_analysis,
// };
const updatedTemplate = {
...selectedTemplate,
storyRole: selectedTemplate.storyRole.map((role, index) =>
index === activeRoleIndex
? {
...role,
photo_url: imageUrl,
}
: role
),
};
setSelectedTemplate(updatedTemplate);
} catch (error) {
message.error("Image analysis failed");
console.log("error", error);
}
},
[selectedTemplate, activeRoleIndex]
);
/**
* URL
*/
const setActiveRoleAudio = useCallback(
(audioUrl: string): void => {
if (
!selectedTemplate ||
activeRoleIndex < 0 ||
activeRoleIndex >= selectedTemplate.storyRole.length
) {
return;
}
const updatedTemplate = {
...selectedTemplate,
storyRole: selectedTemplate.storyRole.map((role, index) =>
index === activeRoleIndex ? { ...role, voice_url: audioUrl } : role
),
};
setSelectedTemplate(updatedTemplate);
},
[selectedTemplate, activeRoleIndex]
);
/**
*
*/
const updateFillableContentField = useCallback(
(fieldName: string, fieldValue: string): void => {
if (!selectedTemplate) return;
const updatedTemplate = {
...selectedTemplate,
fillable_content: selectedTemplate.fillable_content.map((field) =>
field.field_name === fieldName
? { ...field, value: fieldValue }
: field
),
};
setSelectedTemplate(updatedTemplate);
},
[selectedTemplate]
);
/**
*
* @param {string} imageUrl - URL
* @param {string} roleName - 使
* @param {string} roleName -
*/
const AvatarAndAnalyzeFeatures = useCallback(
async (imageUrl: string, roleName?: string): Promise<void> => {
async (imageUrl: string, roleName: string): Promise<void> => {
try {
setIsLoading(true);
// 如果提供了角色名称,更新指定角色;否则更新当前活跃角色
if (roleName) {
updateRoleImage(roleName, imageUrl);
} else {
setActiveRoleData(imageUrl);
}
// 调用用例处理人物头像上传和特征分析
// const result = await templateStoryUseCase.AvatarAndAnalyzeFeatures(
// imageUrl
// );
// 直接更新指定角色的图片
updateRoleImage(roleName, imageUrl);
} catch (error) {
console.error("人物头像上传和特征分析失败:", error);
throw error;
@ -231,7 +98,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
setIsLoading(false);
}
},
[setActiveRoleData]
[]
);
/**
@ -256,13 +123,37 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
},
[selectedTemplate]
);
/**
*
* @param {string} fieldName -
/**
*
* @param {string} itemName -
* @param {string} imageUrl - URL
*/
const updateItemImage = useCallback(
(itemName: string, imageUrl: string): void => {
if (!selectedTemplate) return;
const updatedTemplate = {
...selectedTemplate,
storyItem: selectedTemplate.storyItem.map((item) =>
item.item_name === itemName
? { ...item, photo_url: imageUrl }
: item
),
};
setSelectedTemplate(updatedTemplate);
},
[selectedTemplate]
);
/**
* -
* @param {string} roleName -
* @param {string} fieldValue -
*/
const handleFieldBlur = useCallback(
debounce(async (fieldName: string, fieldValue: string): Promise<void> => {
const handleRoleFieldBlur = useCallback(
debounce(async (roleName: string, fieldValue: string): Promise<void> => {
try {
// 设置 loading 状态
setIsLoading(true);
@ -274,13 +165,13 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
if (result.successful && result.data?.image_url) {
// 更新对应角色的图片
updateRoleImage(fieldName, result.data.image_url);
console.log(`字段 ${fieldName} 图片生成成功:`, result.data.image_url);
updateRoleImage(roleName, result.data.image_url);
console.log(`角色 ${roleName} 图片生成成功:`, result.data.image_url);
} else {
console.error(`字段 ${fieldName} 图片生成失败:`, result.message);
console.error(`角色 ${roleName} 图片生成失败:`, result.message);
}
} catch (error) {
console.error(`字段 ${fieldName} 处理失败:`, error);
console.error(`角色 ${roleName} 处理失败:`, error);
} finally {
// 清除 loading 状态
setIsLoading(false);
@ -289,6 +180,39 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
[updateRoleImage, setIsLoading]
);
/**
* -
* @param {string} itemName -
* @param {string} fieldValue -
*/
const handleItemFieldBlur = useCallback(
debounce(async (itemName: string, fieldValue: string): Promise<void> => {
try {
// 设置 loading 状态
setIsLoading(true);
// 调用图片生成接口
const result = await generateTextToImage({
description: fieldValue
});
if (result.successful && result.data?.image_url) {
// 更新对应道具的图片
updateItemImage(itemName, result.data.image_url);
console.log(`道具 ${itemName} 图片生成成功:`, result.data.image_url);
} else {
console.error(`道具 ${itemName} 图片生成失败:`, result.message);
}
} catch (error) {
console.error(`道具 ${itemName} 处理失败:`, error);
} finally {
// 清除 loading 状态
setIsLoading(false);
}
}, 500),
[updateItemImage, setIsLoading]
);
const actionStory = useCallback(
async (
user_id: string,
@ -308,9 +232,9 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
mode,
resolution,
storyRole: selectedTemplate?.storyRole || [],
storyItem: selectedTemplate?.storyItem || [],
language,
template_id: selectedTemplate?.template_id || "",
fillable_content: selectedTemplate?.fillable_content || [],
};
console.log("params", params);
const result = await MovieProjectService.createProject(
@ -327,25 +251,22 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
},
[selectedTemplate]
);
return {
templateStoryList,
selectedTemplate,
activeRoleIndex,
activeRole,
isLoading,
getTemplateStoryList,
actionStory,
setSelectedTemplate,
setActiveRoleIndex: handleSetActiveRoleIndex,
setActiveRoleAudio,
AvatarAndAnalyzeFeatures,
updateRoleImage,
updateFillableContentField,
handleFieldBlur,
updateItemImage,
handleRoleFieldBlur,
handleItemFieldBlur,
clearData: () => {
setTemplateStoryList([]);
setSelectedTemplate(null);
setActiveRoleIndex(0);
},
};
};

View File

@ -144,35 +144,32 @@ export interface StoryTemplateEntity {
category: string;
/** 故事模板ID */
template_id: string;
/**故事角色 */
/** 故事角色 */
storyRole: {
/**角色名 */
/** 角色名 */
role_name: string;
/**角色描述 对应后端是单个的character_brief */
role_description: {
name: string;
image_url: string;
character_analysis: Record<string, any>;
};
/**照片URL */
/** 角色描述ai分析出来用于剧本生成 */
role_description: string;
/** 用户提示,提示给用户需要输入什么内容 */
user_tips: string;
/** 约束,可选,用于传给ai让ai去拦截用户不符合约束的输入内容 */
constraints: string;
/** 照片URL */
photo_url: string;
/**声音URL */
/** 声音URL */
voice_url: string;
}[];
/**可填的内容 */
fillable_content: {
/** 字段名称 */
field_name: string;
/** 字段类型 */
field_type: string;
/** 字段值 */
field_value?: string;
/** 前端展示,无关后端的字段值 */
value?: string;
/** 字段描述 */
field_description?: string;
/** 字段元数据 */
field_meta?: Record<string, any>;
/** 道具 */
storyItem: {
/** 道具名 */
item_name: string;
/** 道具描述ai分析出来用于剧本生成 */
item_description: string;
/** 用户提示,提示给用户需要输入什么内容 */
user_tips: string;
/** 约束,可选,用于传给ai让ai去拦截用户不符合约束的输入内容 */
constraints: string;
/** 道具照片URL */
photo_url: string;
}[];
}

File diff suppressed because one or more lines are too long

View File

@ -45,6 +45,7 @@ export class RoleEditUseCase {
if (response.successful) {
const roleList = this.parseProjectRoleList(response.data);
console.log('roleList', roleList)
return roleList;
} else {
throw new Error(response.message || '获取项目角色列表失败');
@ -93,13 +94,13 @@ export class RoleEditUseCase {
let draftRoleList:Record<string,RoleEntity> = {};
// 如果草稿箱有数据,则返回草稿箱数据
if(projectRoleData.character_draft){
const roleList = JSON.parse(projectRoleData.character_draft);
const roleList = JSON.parse(projectRoleData.character_draft||"[]");
for(const role of roleList){
draftRoleList[role.name] = role;
}
}
return projectRoleData.data.map((char, index) => {
return projectRoleData?.data?.map((char, index) => {
if(draftRoleList[char.character_name]){
return {
...draftRoleList[char.character_name],
@ -127,7 +128,7 @@ export class RoleEditUseCase {
};
return roleEntity;
});
})||[];
}
/**

View File

@ -19,6 +19,7 @@ export default function SignupPage() {
const [confirmPasswordError, setConfirmPasswordError] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [agreeToTerms, setAgreeToTerms] = useState(false);
const router = useRouter();
/** Password validation function with English prompts */
@ -29,13 +30,23 @@ export default function SignupPage() {
if (password.length > 18) {
return "Password cannot exceed 18 characters";
}
if (!/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d]{8,18}$/.test(password)) {
if (!/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^*&]{8,18}$/.test(password)) {
return "Password must contain both letters and numbers";
}
return "";
};
/** 处理密码输入变化 */
/** Handle Terms of Service click */
const handleTermsClick = () => {
window.open("/Terms", "_blank");
};
/** Handle Privacy Policy click */
const handlePrivacyClick = () => {
window.open("/Privacy", "_blank");
};
/** 处理密码输入变化 */
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newPassword = e.target.value;
setPassword(newPassword);
@ -58,7 +69,9 @@ export default function SignupPage() {
};
/** 处理确认密码输入变化 */
const handleConfirmPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleConfirmPasswordChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const newConfirmPassword = e.target.value;
setConfirmPassword(newConfirmPassword);
@ -73,7 +86,7 @@ export default function SignupPage() {
}
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 验证密码
@ -89,6 +102,12 @@ export default function SignupPage() {
return;
}
// 验证是否同意条款
if (!agreeToTerms) {
setFormError("Please agree to the Terms of Service and Privacy Policy");
return;
}
setIsSubmitting(true);
setFormError("");
@ -105,13 +124,12 @@ export default function SignupPage() {
router.push("/login?registered=true");
} catch (error: any) {
console.error("Signup error:", error);
setFormError("Registration failed, please try again");
setFormError(error.msg || "Registration failed, please try again");
} finally {
setIsSubmitting(false);
}
};
return (
<div className="min-h-screen relative overflow-hidden">
{/* 背景视频 */}
@ -138,7 +156,7 @@ export default function SignupPage() {
data-alt="logo-container"
onClick={() => router.push("/")}
>
<span className="logo-heart">
<span className="logo-heart cursor-pointer">
<GradientText
text="MovieFlow"
startPercentage={30}
@ -162,6 +180,19 @@ export default function SignupPage() {
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-white mb-1">
Email
</label>
<input
type="email"
placeholder="your@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-4 py-3 rounded-lg bg-black/30 border border-white/20 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-white mb-1">
Name
@ -178,86 +209,82 @@ export default function SignupPage() {
<div>
<label className="block text-sm font-medium text-white mb-1">
Email
Password
</label>
<input
type="email"
placeholder="your@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-4 py-3 rounded-lg bg-black/30 border border-white/20 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
placeholder="8-18 characters, letters, numbers and !@#$%^*&"
value={password}
onChange={handlePasswordChange}
required
className={`w-full px-4 py-3 pr-12 rounded-lg bg-black/30 border text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent ${
passwordError ? "border-red-500/50" : "border-white/20"
}`}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white transition-colors"
data-alt="toggle-password-visibility"
>
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
</button>
</div>
<div>
<label className="block text-sm font-medium text-white mb-1 mt-3">
Confirm Password
</label>
<div className="relative">
<input
type={showConfirmPassword ? "text" : "password"}
placeholder="Confirm your password"
value={confirmPassword}
onChange={handleConfirmPasswordChange}
required
className={`w-full px-4 py-3 pr-12 rounded-lg bg-black/30 border text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent ${
confirmPasswordError
? "border-red-500/50"
: "border-white/20"
}`}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white transition-colors"
data-alt="toggle-confirm-password-visibility"
>
{showConfirmPassword ? (
<EyeOff size={20} />
) : (
<Eye size={20} />
)}
</button>
</div>
</div>
{passwordError && (
<p className="mt-1 text-sm text-red-400">{passwordError}</p>
)}
{confirmPasswordError && (
<p className="mt-1 text-sm text-red-400">
{confirmPasswordError}
</p>
)}
{password && !passwordError && (
<p className="mt-1 text-sm text-green-400">
Password format is correct
</p>
)}
{confirmPassword && !confirmPasswordError && (
<p className="mt-1 text-sm text-green-400"> Passwords match</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-white mb-1">
Password
</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
placeholder="8-18 characters, letters and numbers"
value={password}
onChange={handlePasswordChange}
required
className={`w-full px-4 py-3 pr-12 rounded-lg bg-black/30 border text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent ${
passwordError ? "border-red-500/50" : "border-white/20"
}`}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white transition-colors"
data-alt="toggle-password-visibility"
>
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
</button>
</div>
<div>
<label className="block text-sm font-medium text-white mb-1 mt-3">
Confirm Password
</label>
<div className="relative">
<input
type={showConfirmPassword ? "text" : "password"}
placeholder="Confirm your password"
value={confirmPassword}
onChange={handleConfirmPasswordChange}
required
className={`w-full px-4 py-3 pr-12 rounded-lg bg-black/30 border text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent ${
confirmPasswordError ? "border-red-500/50" : "border-white/20"
}`}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white transition-colors"
data-alt="toggle-confirm-password-visibility"
>
{showConfirmPassword ? <EyeOff size={20} /> : <Eye size={20} />}
</button>
</div>
</div>
{passwordError && (
<p className="mt-1 text-sm text-red-400">{passwordError}</p>
)}
{confirmPasswordError && (
<p className="mt-1 text-sm text-red-400">{confirmPasswordError}</p>
)}
{password && !passwordError && (
<p className="mt-1 text-sm text-green-400"> Password format is correct</p>
)}
{confirmPassword && !confirmPasswordError && (
<p className="mt-1 text-sm text-green-400"> Passwords match</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-white mb-1">
Invite Code (Optional)
Invite Code
</label>
<input
type="text"
@ -268,6 +295,33 @@ export default function SignupPage() {
/>
</div>
<div className="flex items-start space-x-3">
<label
htmlFor="agreeToTerms"
className="text-sm text-gray-300 leading-relaxed"
data-alt="terms-privacy-info"
>
By clicking Sign Up, you agree to our{' '}
<button
type="button"
onClick={handleTermsClick}
className="text-purple-400 hover:text-purple-300 underline"
data-alt="terms-link"
>
Terms of Service
</button>{' '}
and acknowledge that you have read and understand our{' '}
<button
type="button"
onClick={handlePrivacyClick}
className="text-purple-400 hover:text-purple-300 underline"
data-alt="privacy-link"
>
Privacy Policy
</button>
</label>
</div>
{formError && (
<div className="bg-red-500/20 text-red-300 p-3 rounded-lg border border-red-500/20">
{formError}
@ -283,7 +337,14 @@ export default function SignupPage() {
</Link>
<button
type="submit"
disabled={isSubmitting || !!passwordError || !!confirmPasswordError || !password || !confirmPassword}
disabled={
isSubmitting ||
!!passwordError ||
!!confirmPasswordError ||
!password ||
!confirmPassword ||
!agreeToTerms
}
className="flex-1 py-3 rounded-lg cursor-pointer bg-[#C039F6] hover:bg-[#C039F6]/80 text-white font-medium transition-colors disabled:opacity-70"
>
{isSubmitting ? "Signing up..." : "Sign Up"}

View File

@ -35,7 +35,10 @@ import { AudioRecorder } from "./AudioRecorder";
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
import { useRouter } from "next/navigation";
import { createMovieProjectV1 } from "@/api/video_flow";
import { MovieProjectService, MovieProjectMode } from "@/app/service/Interaction/MovieProjectService";
import {
MovieProjectService,
MovieProjectMode,
} from "@/app/service/Interaction/MovieProjectService";
import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service";
import { ActionButton } from "../common/ActionButton";
import { HighlightEditor } from "../common/HighlightEditor";
@ -65,17 +68,15 @@ const RenderTemplateStoryMode = ({
const {
templateStoryList,
selectedTemplate,
activeRoleIndex,
activeRole,
isLoading,
getTemplateStoryList,
actionStory,
setSelectedTemplate,
setActiveRoleIndex,
AvatarAndAnalyzeFeatures,
setActiveRoleAudio,
updateFillableContentField,
handleFieldBlur,
updateRoleImage,
updateItemImage,
handleRoleFieldBlur,
handleItemFieldBlur,
clearData,
} = useTemplateStoryServiceHook();
@ -118,7 +119,6 @@ const RenderTemplateStoryMode = ({
// 处理模板选择
const handleTemplateSelect = (template: StoryTemplateEntity) => {
setSelectedTemplate(template);
setActiveRoleIndex(0); // 重置角色选择
};
// 处理确认操作
@ -150,21 +150,18 @@ const RenderTemplateStoryMode = ({
);
if (projectId) {
// 跳转到电影详情页
router.push(`/create/work-flow?episodeId=${projectId}`);
router.push(`/movies/work-flow?episodeId=${projectId}`);
onClose();
// 重置状态
setSelectedTemplate(null);
setActiveRoleIndex(0);
}
console.log("Story action created:", projectId);
} catch (error) {
console.error("Failed to create story action:", error);
// 这里可以添加 toast 提示
onClose();
// 重置状态
setSelectedTemplate(null);
setActiveRoleIndex(0);
} finally {
setLocalLoading(0);
clearInterval(timer);
@ -236,21 +233,21 @@ const RenderTemplateStoryMode = ({
</div>
</div>
</div>
{/* 变量字段填写区域 */}
{selectedTemplate?.fillable_content &&
selectedTemplate.fillable_content.length > 0 && (
{/* 角色配置区域 */}
{selectedTemplate?.storyRole &&
selectedTemplate.storyRole.length > 0 && (
<div className="pt-2 border-t border-white/10">
<h3
data-alt="variables-section-title"
data-alt="roles-section-title"
className="text-lg font-semibold text-white mb-4"
>
Template Configuration
Character Configuration
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{selectedTemplate.fillable_content.map((field, index) => (
{selectedTemplate.storyRole.map((role, index) => (
<div
key={index}
data-alt={`variable-field-${index}`}
data-alt={`role-field-${index}`}
className="flex flex-col items-center space-y-3"
>
{/* 图片容器 */}
@ -260,14 +257,24 @@ const RenderTemplateStoryMode = ({
<div className="relative">
<input
type="text"
value={field.value || ""}
onChange={(e) =>
updateFillableContentField(
field.field_name,
e.target.value
)
}
placeholder={`${field.field_value}`}
value={role.role_description || ""}
onChange={(e) => {
// 更新角色的描述字段
const updatedTemplate = {
...selectedTemplate!,
storyRole: selectedTemplate!.storyRole.map(
(r) =>
r.role_name === role.role_name
? {
...r,
role_description: e.target.value,
}
: r
),
};
setSelectedTemplate(updatedTemplate);
}}
placeholder={role.user_tips}
className="w-[30rem] px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2">
@ -275,13 +282,18 @@ const RenderTemplateStoryMode = ({
<ActionButton
isCreating={false}
handleCreateVideo={() => {
handleFieldBlur(
field.field_name,
field.value || ""
);
if (
role.role_description &&
role.role_description.trim()
) {
handleRoleFieldBlur(
role.role_name,
role.role_description.trim()
);
}
setInputVisible((prev) => ({
...prev,
[field.field_name]: false,
[role.role_name]: false,
}));
}}
icon={<Sparkles className="w-4 h-4" />}
@ -295,11 +307,11 @@ const RenderTemplateStoryMode = ({
classNames={{
root: "max-w-none",
}}
open={inputVisible[field.field_name]}
open={inputVisible[role.role_name]}
onOpenChange={(visible) =>
setInputVisible((prev) => ({
...prev,
[field.field_name]: visible,
[role.role_name]: visible,
}))
}
trigger="contextMenu"
@ -307,16 +319,12 @@ const RenderTemplateStoryMode = ({
>
{/* 图片 */}
<div
data-alt={`field-thumbnail-${index}`}
data-alt={`role-thumbnail-${index}`}
className="w-24 h-24 rounded-xl overflow-hidden border border-white/10 bg-white/0 flex items-center justify-center cursor-pointer hover:scale-105 transition-all duration-200"
>
<Image
src={
selectedTemplate?.storyRole?.find(
(role) => role.role_name === field.field_name
)?.photo_url || "/assets/empty_video.png"
}
alt={field.field_name}
src={role.photo_url || "/assets/empty_video.png"}
alt={role.role_name}
className="w-full h-full object-cover"
preview={{
mask: null,
@ -330,7 +338,7 @@ const RenderTemplateStoryMode = ({
{/* 角色名称 - 图片下方 */}
<div className="text-center mt-2">
<span className="text-white text-sm font-medium">
{field.field_name}
{role.role_name}
</span>
</div>
@ -339,11 +347,11 @@ const RenderTemplateStoryMode = ({
{/* AI生成按钮 */}
<Tooltip title="AI generate image" placement="top">
<button
data-alt={`field-ai-button-${index}`}
data-alt={`role-ai-button-${index}`}
onClick={() =>
setInputVisible((prev) => ({
...prev,
[field.field_name]: !prev[field.field_name],
[role.role_name]: !prev[role.role_name],
}))
}
className="w-6 h-6 bg-purple-500 hover:bg-purple-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
@ -354,7 +362,7 @@ const RenderTemplateStoryMode = ({
{/* 上传按钮 */}
<Upload
name="fieldImage"
name="roleImage"
showUploadList={false}
beforeUpload={(file) => {
const isImage = file.type.startsWith("image/");
@ -384,18 +392,200 @@ const RenderTemplateStoryMode = ({
);
await AvatarAndAnalyzeFeatures(
uploadedUrl,
field.field_name
role.role_name
);
onSuccess?.(uploadedUrl);
} catch (error) {
console.error("字段图片上传失败:", error);
console.error("角色图片上传失败:", error);
onError?.(error as Error);
}
}}
>
<Tooltip title="upload your image" placement="top">
<button
data-alt={`field-upload-button-${index}`}
data-alt={`role-upload-button-${index}`}
className="w-6 h-6 bg-blue-500 hover:bg-blue-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
>
<UploadOutlined className="w-3.5 h-3.5" />
</button>
</Tooltip>
</Upload>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* 道具配置区域 */}
{selectedTemplate?.storyItem &&
selectedTemplate.storyItem.length > 0 && (
<div className="pt-2 border-t border-white/10">
<h3
data-alt="items-section-title"
className="text-lg font-semibold text-white mb-4"
>
props Configuration
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{selectedTemplate.storyItem.map((item, index) => (
<div
key={index}
data-alt={`item-field-${index}`}
className="flex flex-col items-center space-y-3"
>
{/* 图片容器 */}
<div className="relative group">
<Tooltip
title={
<div className="relative">
<input
type="text"
value={item.item_description || ""}
onChange={(e) => {
// 更新道具的描述字段
const updatedTemplate = {
...selectedTemplate!,
storyItem: selectedTemplate!.storyItem.map(
(i) =>
i.item_name === item.item_name
? {
...i,
item_description: e.target.value,
}
: i
),
};
setSelectedTemplate(updatedTemplate);
}}
placeholder="Enter description for AI image generation..."
className="w-[30rem] px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2">
{/* AI生成按钮 */}
<ActionButton
isCreating={false}
handleCreateVideo={() => {
if (
item.item_description &&
item.item_description.trim()
) {
handleItemFieldBlur(
item.item_name,
item.item_description.trim()
);
}
setInputVisible((prev) => ({
...prev,
[item.item_name]: false,
}));
}}
icon={<Sparkles className="w-4 h-4" />}
width="w-8"
height="h-8"
/>
</div>
</div>
}
placement="top"
classNames={{
root: "max-w-none",
}}
open={inputVisible[item.item_name]}
onOpenChange={(visible) =>
setInputVisible((prev) => ({
...prev,
[item.item_name]: visible,
}))
}
trigger="contextMenu"
styles={{ root: { zIndex: 1000 } }}
>
{/* 图片 */}
<div
data-alt={`item-thumbnail-${index}`}
className="w-24 h-24 rounded-xl overflow-hidden border border-white/10 bg-white/0 flex items-center justify-center cursor-pointer hover:scale-105 transition-all duration-200"
>
<Image
src={item.photo_url || "/assets/empty_video.png"}
alt={item.item_name}
className="w-full h-full object-cover"
preview={{
mask: null,
maskClassName: "hidden",
}}
fallback="/assets/empty_video.png"
/>
</div>
</Tooltip>
{/* 道具名称 - 图片下方 */}
<div className="text-center mt-2">
<span className="text-white text-sm font-medium">
{item.item_name}
</span>
</div>
{/* 按钮组 - 右上角 */}
<div className="absolute -top-8 left-[1.2rem] flex gap-3 opacity-0 group-hover:opacity-100 transition-all duration-200">
{/* AI生成按钮 */}
<Tooltip title="AI generate image" placement="top">
<button
data-alt={`item-ai-button-${index}`}
onClick={() =>
setInputVisible((prev) => ({
...prev,
[item.item_name]: !prev[item.item_name],
}))
}
className="w-6 h-6 bg-purple-500 hover:bg-purple-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
>
<Sparkles className="w-3.5 h-3.5" />
</button>
</Tooltip>
{/* 上传按钮 */}
<Upload
name="itemImage"
showUploadList={false}
beforeUpload={(file) => {
const isImage = file.type.startsWith("image/");
if (!isImage) {
console.error("只能上传图片文件");
return false;
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
console.error("图片大小不能超过5MB");
return false;
}
return true;
}}
customRequest={async ({
file,
onSuccess,
onError,
}) => {
try {
const fileObj = file as File;
const uploadedUrl = await uploadFile(
fileObj,
(progress) => {
console.log(`上传进度: ${progress}%`);
}
);
updateItemImage(item.item_name, uploadedUrl);
onSuccess?.(uploadedUrl);
} catch (error) {
console.error("道具图片上传失败:", error);
onError?.(error as Error);
}
}}
>
<Tooltip title="upload your image" placement="top">
<button
data-alt={`item-upload-button-${index}`}
className="w-6 h-6 bg-blue-500 hover:bg-blue-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
>
<UploadOutlined className="w-3.5 h-3.5" />
@ -558,6 +748,7 @@ const RenderTemplateStoryMode = ({
footer={null}
width="60%"
closable={false}
maskClosable={false}
style={{ maxWidth: "800px", marginTop: "0vh" }}
className="photo-story-modal !pb-0 rounded-lg bg-white/[0.08] backdrop-blur-[20px] [&_.ant-modal-content]:bg-white/[0.00]"
>
@ -662,46 +853,52 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
episodeData
);
const episodeId = result.project_id;
router.push(`/create/work-flow?episodeId=${episodeId}`);
router.push(`/movies/work-flow?episodeId=${episodeId}`);
} catch (error) {
console.error("创建剧集失败:", error);
} finally {
setIsCreating(false);
}
};
return (
<div className="video-tool-component relative max-w-[1080px] w-full left-[50%] translate-x-[-50%]" style={noData ? {
top: '50%'
} : {}}>
<div
className="video-tool-component relative max-w-[1080px] w-full left-[50%] translate-x-[-50%]"
style={
noData
? {
top: "50%",
}
: {}
}
>
{/* 视频故事板工具面板 - 毛玻璃效果背景 */}
<div className="video-storyboard-tools rounded-[20px] bg-white/[0.08] backdrop-blur-[20px] border border-white/[0.12] shadow-[0_8px_32px_rgba(0,0,0,0.3)]">
{/* 展开/收起控制区域 */}
{
!noData && (
<>
{isExpanded ? (
// 展开状态:显示收起按钮和提示
<div
className="absolute top-0 bottom-0 left-0 right-0 z-[1] flex flex-col items-center justify-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer"
onClick={() => setIsExpanded(false)}
>
<ChevronUp className="w-4 h-4 text-white/80" />
<span className="text-sm text-white/80 mt-1">Click to action</span>
</div>
) : (
// 收起状态:显示展开按钮
<div
className="absolute top-[-8px] left-[50%] z-[2] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]"
onClick={() => setIsExpanded(true)}
>
<ChevronDown className="w-4 h-4" />
</div>
)}
</>
)
}
{!noData && (
<>
{isExpanded ? (
// 展开状态:显示收起按钮和提示
<div
className="absolute top-0 bottom-0 left-0 right-0 z-[1] flex flex-col items-center justify-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer"
onClick={() => setIsExpanded(false)}
>
<ChevronUp className="w-4 h-4 text-white/80" />
<span className="text-sm text-white/80 mt-1">
Click to action
</span>
</div>
) : (
// 收起状态:显示展开按钮
<div
className="absolute top-[-8px] left-[50%] z-[2] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]"
onClick={() => setIsExpanded(true)}
>
<ChevronDown className="w-4 h-4" />
</div>
)}
</>
)}
{/* 主要内容区域 - 简化层级,垂直居中 */}
<div
@ -730,7 +927,6 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
placement={"left" as any}
trigger={["click"]}
>
{/* 配置项显示控制按钮 - 齿轮图标 */}
<Tooltip title="config" placement="top">
<button
data-alt="config-toggle-button"
@ -754,9 +950,13 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
onChange={(e) => setScript(e.target.value)}
placeholder="Describe the content you want to action..."
className="w-full pl-[10px] pr-[10px] py-[14px] rounded-[10px] leading-[20px] text-sm border-none bg-transparent text-white placeholder:text-white/[0.40] focus:outline-none resize-none min-h-[48px] max-h-[120px] overflow-y-auto"
style={noData ? {
minHeight: '128px'
} : {}}
style={
noData
? {
minHeight: "128px",
}
: {}
}
rows={1}
onInput={(e) => {
const target = e.target as HTMLTextAreaElement;
@ -771,7 +971,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
<div className="flex items-center justify-between">
{/* 左侧功能按钮区域 */}
<div className="flex items-center gap-2">
{/* 获取创意按钮 */}
{/*
<Tooltip
title="Get creative ideas for your story"
placement="top"
@ -787,10 +987,10 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
<Lightbulb className="w-4 h-4" />
)}
</button>
</Tooltip>
</Tooltip> */}
{/* 分隔线 */}
<div className="w-px h-4 bg-white/[0.20]"></div>
{/* <div className="w-px h-4 bg-white/[0.20]"></div> */}
{/* 模板故事按钮 */}
<Tooltip title="Choose from movie templates" placement="top">
@ -830,6 +1030,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
isCreating={isCreating}
handleCreateVideo={handleCreateVideo}
icon={<Clapperboard className="w-5 h-5" />}
className="mr-1 mb-1"
/>
</div>
</div>
@ -872,50 +1073,48 @@ const ConfigOptions = ({
onConfigChange: (key: string, value: string) => void;
}) => {
const configItems = [
{
key: "mode",
icon: Package,
options: [
{ value: "auto", label: "Auto", isVip: false },
{ value: "manual", label: "Manual", isVip: true },
],
},
{
key: "resolution",
icon: Video,
options: [
{ value: "720p", label: "720P", isVip: false },
{ value: "1080p", label: "1080P", isVip: true },
{ value: "2k", label: "2K", isVip: true },
{ value: "4k", label: "4K", isVip: true },
],
},
// {
// key: "mode",
// icon: Package,
// options: [
// { value: "auto", label: "Auto", isVip: false },
// { value: "manual", label: "Manual", isVip: true },
// ],
// },
// {
// key: "resolution",
// icon: Video,
// options: [
// { value: "720p", label: "720P", isVip: false },
// { value: "1080p", label: "1080P", isVip: true },
// { value: "2k", label: "2K", isVip: true },
// { value: "4k", label: "4K", isVip: true },
// ],
// },
{
key: "language",
icon: Globe,
options: [
{ value: "english", label: "English", isVip: false },
{ value: "chinese", label: "Chinese", isVip: true },
{ value: "japanese", label: "Japanese", isVip: true },
{ value: "korean", label: "Korean", isVip: true },
{ value: "spanish", label: "Spanish", isVip: true },
{ value: "portuguese", label: "Portuguese", isVip: true },
{ value: "hindi", label: "Hindi", isVip: true },
{ value: "japanese", label: "Japanese", isVip: true },
{ value: "korean", label: "Korean", isVip: true },
{ value: "arabic", label: "Arabic", isVip: true },
{ value: "russian", label: "Russian", isVip: true },
],
},
{
key: "videoDuration",
icon: Clock,
options: [
{ value: "1min", label: "1 Min", isVip: false },
{ value: "2min", label: "2 Min", isVip: true },
{ value: "3min", label: "3 Min", isVip: true },
{ value: "chinese", label: "Chinese", isVip: false },
{ value: "japanese", label: "Japanese", isVip: false },
{ value: "spanish", label: "Spanish", isVip: false },
{ value: "portuguese", label: "Portuguese", isVip: false },
{ value: "hindi", label: "Hindi", isVip: false },
{ value: "korean", label: "Korean", isVip: false },
{ value: "arabic", label: "Arabic", isVip: false },
{ value: "russian", label: "Russian", isVip: false },
],
},
// {
// key: "videoDuration",
// icon: Clock,
// options: [
// { value: "1min", label: "1 Min", isVip: false },
// { value: "2min", label: "2 Min", isVip: true },
// { value: "3min", label: "3 Min", isVip: true },
// ],
// },
];
return (
@ -1061,7 +1260,7 @@ const PhotoStoryModal = ({
if (!episodeResponse) return;
let episodeId = episodeResponse.project_id;
// let episodeId = '9c34fcc4-c8d8-44fc-879e-9bd56f608c76';
router.push(`/create/work-flow?episodeId=${episodeId}`);
router.push(`/movies/work-flow?episodeId=${episodeId}`);
// 成功后关闭弹窗
handleClose();
} catch (error) {
@ -1095,6 +1294,7 @@ const PhotoStoryModal = ({
onCancel={handleClose}
footer={null}
width="80%"
maskClosable={false}
style={{ maxWidth: "1000px", marginTop: "10vh" }}
className="photo-story-modal bg-white/[0.08] backdrop-blur-[20px] [&_.ant-modal-content]:bg-white/[0.00]"
closeIcon={

View File

@ -100,66 +100,73 @@ export const showQueueNotification = (
status: string,
onCancel: () => void
) => {
notification.open({
message: null,
description: (
<div data-alt="queue-notification" style={{ minWidth: '320px' }}>
{/* AI导演工作室场景 */}
<div style={studioContainerStyle}>
<AIDirector />
<Workstation />
<ProgressTimeline />
</div>
{/* 队列信息 */}
<div style={{
fontSize: '13px',
color: 'rgba(255, 255, 255, 0.9)',
marginBottom: '12px',
display: 'flex',
alignItems: 'center',
background: 'rgba(246, 178, 102, 0.1)',
padding: '8px 12px',
borderRadius: '6px',
}}>
<span style={{ marginRight: '8px' }}>🎬</span>
{status === 'process' ? `Your work is being produced. Please wait until it is completed before creating a new work.` : `Your work is waiting for production at the ${position} position`}
</div>
{/* 预计等待时间 */}
<div style={{
fontSize: '12px',
color: 'rgba(255, 255, 255, 0.65)',
marginBottom: '12px',
}}>
{status !== 'process' && `Estimated waiting time: about ${estimatedMinutes} minutes`}
</div>
{/* 取消按钮 */}
<button
onClick={() => {
onCancel?.();
notification.destroy();
}}
style={{
color: 'rgb(250 173 20 / 90%)',
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: 0,
fontSize: '12px',
fontWeight: 500,
textDecoration: 'underline',
textUnderlineOffset: '2px',
textDecorationColor: 'rgb(250 173 20 / 60%)',
transition: 'all 0.2s ease',
}}
data-alt="cancel-queue-button"
>
Cancel queue
</button>
const notificationKey = 'queueNotification';
// 创建或更新通知内容
const notificationContent = (
<div data-alt="queue-notification" style={{ minWidth: '320px' }}>
{/* AI导演工作室场景 */}
<div style={studioContainerStyle}>
<AIDirector />
<Workstation />
<ProgressTimeline />
</div>
),
{/* 队列信息 */}
<div style={{
fontSize: '13px',
color: 'rgba(255, 255, 255, 0.9)',
marginBottom: '12px',
display: 'flex',
alignItems: 'center',
background: 'rgba(246, 178, 102, 0.1)',
padding: '8px 12px',
borderRadius: '6px',
}}>
<span style={{ marginRight: '8px' }}>🎬</span>
{status === 'process' ? `Your work is being produced. Please wait until it is completed before creating a new work.` : `Your work is waiting for production at the ${position} position`}
</div>
{/* 预计等待时间 */}
<div style={{
fontSize: '12px',
color: 'rgba(255, 255, 255, 0.65)',
marginBottom: '12px',
}}>
{status !== 'process' && `Estimated waiting time: about ${estimatedMinutes} minutes`}
</div>
{/* 取消按钮 */}
<button
onClick={() => {
onCancel?.();
notification.destroy(notificationKey);
}}
style={{
color: 'rgb(250 173 20 / 90%)',
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: 0,
fontSize: '12px',
fontWeight: 500,
textDecoration: 'underline',
textUnderlineOffset: '2px',
textDecorationColor: 'rgb(250 173 20 / 60%)',
transition: 'all 0.2s ease',
}}
data-alt="cancel-queue-button"
>
Cancel queue
</button>
</div>
);
// 打开或更新通知
notification.open({
key: notificationKey,
message: null,
description: notificationContent,
duration: 0,
placement: 'topRight',
style: {
@ -167,23 +174,7 @@ export const showQueueNotification = (
border: '1px solid rgba(246, 178, 102, 0.2)',
},
className: 'director-studio-notification',
closeIcon: (
<button
className="hover:text-white"
style={{
background: 'transparent',
border: 'none',
padding: '2px',
cursor: 'pointer',
color: 'rgba(255, 255, 255, 0.45)',
transition: 'color 0.2s ease',
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
),
closeIcon: null,
});
};

View File

@ -3,6 +3,7 @@ import { Image as ImageIcon, Send, Trash2, ArrowUp } from "lucide-react";
import { MessageBlock } from "./types";
import { useUploadFile } from "@/app/service/domain/service";
import { motion, AnimatePresence } from "framer-motion";
import { QuickActionTags, QuickAction } from "./QuickActionTags";
// 防抖函数
function debounce<T extends (...args: any[]) => void>(func: T, wait: number) {
@ -233,6 +234,19 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
</div>
)}
{/* 快捷操作标签组 */}
<QuickActionTags
onTagClick={(action: QuickAction) => {
// 将标签文本添加到输入框
setText(action.label);
// 聚焦输入框并触发高度调整
if (textareaRef.current) {
textareaRef.current.focus();
adjustHeight();
}
}}
/>
<motion.div
layout
className="px-3 m-3 border border-gray-700 rounded-[2rem]"

View File

@ -111,7 +111,7 @@ export function MessageRenderer({ msg }: MessageRendererProps) {
src={b.url}
poster={b.poster}
className="w-full max-h-80 bg-black"
controlsList="nodownload noremoteplayback"
controlsList="noremoteplayback"
disablePictureInPicture
disableRemotePlayback
onContextMenu={e => e.preventDefault()}
@ -125,7 +125,7 @@ export function MessageRenderer({ msg }: MessageRendererProps) {
case "progress":
return <ProgressBar key={idx} value={b.value} total={b.total} label={b.label} />;
case "link":
return <a key={idx} href={b.url} className="underline">{b.text}</a>;
return <a key={idx} href={b.url} className="underline hover:underline text-[rgb(111 208 211)]">{b.text}</a>;
default:
return null;
}

View File

@ -0,0 +1,123 @@
import React, { useRef, useCallback } from 'react';
import { motion } from 'framer-motion';
import { ChevronLeft, ChevronRight } from 'lucide-react';
/** 快捷操作标签的数据结构 */
export interface QuickAction {
id: string;
label: string;
}
/** 预设的快捷操作标签 */
export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [
{ id: 'weather', label: 'Change video scene weather' },
{ id: 'character', label: 'Change a character in the video' },
{ id: 'costume', label: 'Change the clothing of a character in the video' },
{ id: 'scene', label: 'Change video scene background' },
{ id: 'action', label: 'Change character action' }
];
interface QuickActionTagsProps {
/** 自定义标签列表,如果不提供则使用默认标签 */
actions?: QuickAction[];
/** 点击标签时的回调函数 */
onTagClick: (action: QuickAction) => void;
}
/**
*
* @param props
* @returns JSX.Element
*/
export function QuickActionTags({ actions = DEFAULT_QUICK_ACTIONS, onTagClick }: QuickActionTagsProps) {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const scroll = useCallback((direction: 'left' | 'right') => {
const container = scrollContainerRef.current;
if (!container) return;
const scrollAmount = 200; // 每次滚动的距离
const targetScroll = container.scrollLeft + (direction === 'left' ? -scrollAmount : scrollAmount);
container.scrollTo({
left: targetScroll,
behavior: 'smooth'
});
}, []);
return (
<div
data-alt="quick-action-tags"
className="relative flex items-center px-3 py-2 group"
>
{/* 左侧渐变遮罩 */}
<div className="absolute left-0 top-0 bottom-0 w-12 bg-gradient-to-r from-black/20 to-transparent pointer-events-none z-10" />
{/* 左滚动按钮 */}
<motion.button
onClick={() => scroll('left')}
className="absolute left-1 z-20 p-1 rounded-full bg-black/30 text-white/80
backdrop-blur-sm opacity-0 group-hover:opacity-100 transition-opacity
hover:bg-black/40"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
data-alt="scroll-left"
>
<ChevronLeft size={16} />
</motion.button>
{/* 标签滚动容器 */}
<div
ref={scrollContainerRef}
className="flex overflow-x-auto gap-2 no-scrollbar scroll-smooth"
style={{
msOverflowStyle: 'none', /* IE and Edge */
scrollbarWidth: 'none', /* Firefox */
}}
>
{actions.map((action) => (
<motion.button
key={action.id}
onClick={() => onTagClick(action)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="flex-none px-[8px] py-[3px] rounded-full text-[10px] text-white/80
backdrop-blur-md bg-white/10 border border-white/20
hover:bg-white/20 hover:text-white
transition-colors duration-200
shadow-[0_4px_6px_-1px_rgba(0,0,0,0.1),0_2px_4px_-1px_rgba(0,0,0,0.06)]"
data-alt={`quick-action-${action.id}`}
>
{action.label}
</motion.button>
))}
</div>
{/* 右侧渐变遮罩 */}
<div className="absolute right-0 top-0 bottom-0 w-12 bg-gradient-to-l from-black/20 to-transparent pointer-events-none z-10" />
{/* 右滚动按钮 */}
<motion.button
onClick={() => scroll('right')}
className="absolute right-1 z-20 p-1 rounded-full bg-black/30 text-white/80
backdrop-blur-sm opacity-0 group-hover:opacity-100 transition-opacity
hover:bg-black/40"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
data-alt="scroll-right"
>
<ChevronRight size={16} />
</motion.button>
</div>
);
}
// 添加全局样式来隐藏滚动条
const style = document.createElement('style');
style.textContent = `
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
`;
document.head.appendChild(style);

View File

@ -1,5 +1,5 @@
import React, { useRef, useCallback, useState, useEffect } from "react";
import { ArrowRightFromLine, ChevronDown } from 'lucide-react';
import { ChevronsRight, ChevronDown } from 'lucide-react';
import { Switch } from 'antd';
import { MessageRenderer } from "./MessageRenderer";
import { InputBar } from "./InputBar";
@ -133,16 +133,16 @@ export default function SmartChatBox({
<span>Chat</span>
{/* System push toggle */}
<Switch
checkedChildren="System push: On"
unCheckedChildren="System push: Off"
checkedChildren="On"
unCheckedChildren="Off"
checked={systemPush}
onChange={toggleSystemPush}
className="ml-2 "
/>
</div>
<div className="text-xs opacity-70">
<ArrowRightFromLine
className="w-4 h-4 cursor-pointer"
<ChevronsRight
className="w-6 h-6 cursor-pointer"
onClick={() => setIsSmartChatBoxOpen(false)}
/>
</div>
@ -168,7 +168,7 @@ export default function SmartChatBox({
</div>
{/* Loading indicator */}
{isLoading && !hasMore && (
{isLoading && (
<div className="flex justify-start space-x-1 p-2">
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:-0.3s]"></span>
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:-0.15s]"></span>

View File

@ -16,7 +16,7 @@ export default function AuthGuard({ children }: AuthGuardProps) {
const pathname = usePathname();
// 不需要鉴权的页面
const publicPaths = ['/','/login', '/signup', '/forgot-password'];
const publicPaths = ['/','/login', '/signup', '/forgot-password', '/Terms', '/Privacy'];
const isPublicPath = publicPaths.includes(pathname);
useEffect(() => {

View File

@ -0,0 +1,90 @@
import React from 'react';
interface UserCardProps {
/** 用户计划名称 */
plan_name?: string;
/** 子组件内容 */
children: React.ReactNode;
}
/**
*
*
*/
export default function UserCard({
plan_name = "none",
children
}: UserCardProps) {
// 根据计划名称获取对应的样式
const getCardStyles = () => {
switch (plan_name) {
case "Kickoff":
return {
outerBg: "bg-[#3d3c3d]",
glowColor: "bg-[#c0c0c0]",
glowShadow: "shadow-[0_0_50px_rgba(192,192,192,0.3)]"
};
case "Pro":
return {
outerBg: "bg-gradient-to-br from-[#1a1a2e] via-[#16213e] to-[#0f3460]",
glowColor: "bg-[#4facfe]",
glowShadow: "shadow-[0_0_50px_rgba(79,172,254,0.4)]"
};
case "Ultra":
return {
outerBg: "bg-gradient-to-br from-[#0a4a4a] via-[#2a1f4a] to-[#4a0a4a]",
glowColor: "bg-gradient-to-r from-[rgb(106,244,249)] to-[rgb(199,59,255)]",
glowShadow: "shadow-[0_0_50px_rgba(106,244,249,0.5)]"
};
default: // none
return {
outerBg: "bg-[#1a1a1a]",
glowColor: "bg-[#333]",
glowShadow: "shadow-[0_0_50px_rgba(51,51,51,0.3)]"
};
}
};
const styles = getCardStyles();
return (
<div
data-alt="user-card-outer"
className={`relative w-[18.75rem] h-[11.5rem] overflow-hidden rounded-xl ${styles.outerBg} ${styles.glowShadow}`}
>
{/* 发光背景 */}
<div
className={`absolute w-56 h-48 ${styles.glowColor} blur-[50px] -left-1/2 -top-1/2 opacity-40`}
/>
{/* 卡片内容区域 */}
<div
data-alt="user-card"
className={`relative z-[1] w-full h-full flex items-center justify-center text-white rounded-xl overflow-hidden `}
style={{ margin: '2px' }}
>
{/* 计划名称背景文字 - 非 none 时显示 */}
{plan_name !== "none" && (
<div
data-alt="plan-name-background"
className="absolute inset-0 flex items-center justify-center z-[1] text-white/10 font-bold text-6xl tracking-wider"
style={{
transform: 'rotate(-15deg) scale(1.2)',
textShadow: '0 0 20px rgba(255,255,255,0.1)'
}}
>
{plan_name}
</div>
)}
{/* 内容区域 */}
<div
data-alt="user-card-content"
className="relative z-[2] w-full h-full flex flex-col items-center justify-center rounded-xl"
>
{children}
</div>
</div>
</div>
);
};

View File

@ -9,12 +9,18 @@ interface DashboardLayoutProps {
}
export function DashboardLayout({ children }: DashboardLayoutProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
const [sidebarCollapsed, setSidebarCollapsed] = useState(true); // 默认收起状态
return (
<div className=" min-h-screen bg-background">
<TopBar collapsed={sidebarCollapsed} onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)} />
{children}
<TopBar collapsed={sidebarCollapsed} />
<Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />
<div className="h-[calc(100vh-4rem)] top-[4rem] fixed right-0 bottom-0 z-[999]" style={{
left: sidebarCollapsed ? '4rem' : '16rem',
width: sidebarCollapsed ? 'calc(100vw - 4rem)' : 'calc(100vw - 16rem)'
}}>
{children}
</div>
</div>
);
}

View File

@ -19,7 +19,8 @@ import {
Video,
PanelsLeftBottom,
ArrowLeftToLine,
X
BookHeart,
PanelRightClose
} from 'lucide-react';
interface SidebarProps {
@ -31,10 +32,7 @@ const navigationItems = [
{
title: 'Main',
items: [
{ name: 'Home', href: '/', icon: Home },
{ name: 'Media Library', href: '/media', icon: FolderOpen },
{ name: 'Actors Library', href: '/actors', icon: Users },
{ name: 'Task History', href: '/history', icon: History },
{ name: 'My Portfolio', href: '/movies', icon: BookHeart },
],
}
];
@ -43,67 +41,56 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
const pathname = usePathname();
return (
<>
{/* Backdrop */}
{!collapsed && (
<div
className="fixed inset-0 bg-[#000000bf] z-[998]"
onClick={() => onToggle(true)}
/>
)}
<>
{/* Sidebar */}
<div
data-alt="sidebar-container"
className={cn(
'fixed left-0 top-0 z-[999] h-full w-64 bg-[#131416] transition-transform duration-300',
collapsed ? '-translate-x-full' : 'translate-x-0'
'fixed left-0 top-0 z-[999] h-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60',
'border-r border-r-[#ffffff14] transition-all duration-300 ease-in-out',
collapsed ? 'w-[3rem]' : 'w-[16rem]'
)}
>
<div className="flex h-full flex-col">
<div className="flex h-16 items-center justify-between px-4">
<div className="flex items-center space-x-2">
<Video className="h-8 w-8 text-primary" />
<span className="text-xl font-bold">
<GradientText
text="MovieFlow"
startPercentage={30}
endPercentage={70}
/>
</span>
</div>
{/* Toggle Button */}
<div className="flex h-16 items-center px-4">
<Button
data-alt="toggle-sidebar"
variant="ghost"
size="sm"
onClick={() => onToggle(true)}
className="button-NxtqWZ"
size="icon"
className={cn(
'h-4 w-4 rounded-full transition-transform duration-300 text-gray-300',
!collapsed && 'rotate-180'
)}
onClick={() => onToggle(!collapsed)}
>
<ArrowLeftToLine className="h-4 w-4" />
<PanelRightClose className="h-4 w-4" />
</Button>
</div>
{/* Navigation */}
<div className="flex-1 overflow-y-auto">
{navigationItems.map((section, index) => (
<div key={section.title}>
<div className="space-y-1">
{section.items.map((item) => {
const isActive = pathname === item.href;
return (
<Link key={item.name} href={item.href}>
<Button
variant={isActive ? 'secondary' : 'ghost'}
className={cn(
'w-full justify-start px-4',
isActive && 'bg-primary/10 text-primary hover:bg-primary/20'
)}
>
<item.icon className="h-4 w-4" />
<span className="ml-2">{item.name}</span>
</Button>
</Link>
);
})}
</div>
<div className="flex-1 space-y-1 px-1">
{navigationItems.map((section) => (
<div key={section.title} className="space-y-1">
{section.items.map((item) => {
const isActive = pathname === item.href;
return (
<Link key={item.name} href={item.href}>
<Button
data-alt={`nav-item-${item.name}`}
variant={isActive ? 'secondary' : 'ghost'}
className={cn(
'w-full justify-start',
collapsed ? 'px-3' : 'px-4',
isActive && 'bg-primary/10 text-primary hover:bg-primary/20'
)}
>
<item.icon className={cn('h-4 w-4 shrink-0 text-gray-300', !collapsed && 'mr-2')} />
{!collapsed && <span>{item.name}</span>}
</Button>
</Link>
);
})}
</div>
))}
</div>

View File

@ -2,6 +2,7 @@
import "../pages/style/top-bar.css";
import { Button } from "@/components/ui/button";
import { GradientText } from "@/components/ui/gradient-text";
import { useTheme } from "next-themes";
import {
@ -12,14 +13,18 @@ import {
LogOut,
PanelsLeftBottom,
Bell,
} from 'lucide-react';
import { motion } from 'framer-motion';
import ReactDOM from 'react-dom';
import { useRouter, usePathname } from 'next/navigation';
import React, { useRef, useEffect, useLayoutEffect, useState } from 'react';
import { logoutUser } from '@/lib/auth';
import { createPortalSession, redirectToPortal, getUserSubscriptionInfo } from '@/lib/stripe';
} from "lucide-react";
import { motion } from "framer-motion";
import ReactDOM from "react-dom";
import { useRouter, usePathname } from "next/navigation";
import React, { useRef, useEffect, useLayoutEffect, useState } from "react";
import { logoutUser } from "@/lib/auth";
import {
createPortalSession,
redirectToPortal,
getUserSubscriptionInfo,
} from "@/lib/stripe";
import UserCard from "@/components/common/userCard";
interface User {
id: string;
@ -27,27 +32,22 @@ interface User {
email: string;
avatar: string;
username: string;
plan_name?: string;
}
export function TopBar({
collapsed,
onToggleSidebar,
}: {
collapsed: boolean;
onToggleSidebar: () => void;
}) {
export function TopBar({ collapsed }: { collapsed: boolean }) {
const router = useRouter();
const [isOpen, setIsOpen] = React.useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const currentUser: User = JSON.parse(
localStorage.getItem("currentUser") || "{}"
const [currentUser, setCurrentUser] = useState<User>(
JSON.parse(localStorage.getItem("currentUser") || "{}")
);
const pathname = usePathname()
const pathname = usePathname();
const [mounted, setMounted] = React.useState(false);
const [isLogin, setIsLogin] = useState(false);
const [isManagingSubscription, setIsManagingSubscription] = useState(false);
const [subscriptionStatus, setSubscriptionStatus] = useState<string>('');
const [subscriptionStatus, setSubscriptionStatus] = useState<string>("");
const [credits, setCredits] = useState<number>(100);
const [isLoadingSubscription, setIsLoadingSubscription] = useState(false);
@ -59,11 +59,18 @@ export function TopBar({
try {
const response = await getUserSubscriptionInfo(String(currentUser.id));
if (response.successful && response.data) {
setSubscriptionStatus(response.data.subscription_status);
const status = response.data.subscription_status;
setSubscriptionStatus(status);
setCredits(response.data.credits);
// 更新 currentUser 的 plan_name
setCurrentUser((prev) => ({
...prev,
plan_name: response.data.plan_name, // HACK
}));
}
} catch (error) {
console.error('获取订阅信息失败:', error);
console.error("获取订阅信息失败:", error);
} finally {
setIsLoadingSubscription(false);
}
@ -86,7 +93,7 @@ export function TopBar({
// 处理订阅管理
const handleManageSubscription = async () => {
if (!currentUser?.id) {
console.error('用户未登录');
console.error("用户未登录");
return;
}
@ -94,7 +101,7 @@ export function TopBar({
try {
const response = await createPortalSession({
user_id: String(currentUser.id),
return_url: window.location.origin + '/dashboard'
return_url: window.location.origin + "/dashboard",
});
if (response.successful && response.data?.portal_url) {
@ -159,10 +166,10 @@ export function TopBar({
return (
<div
className="fixed right-0 top-0 left-0 h-16 header z-[999]"
style={{ isolation: "isolate" }}
className="fixed right-0 top-0 h-16 header z-[999]"
style={{ isolation: "isolate", left: collapsed ? "3rem" : "16rem" }}
>
<div className="h-full flex items-center justify-between pr-6 pl-6">
<div className="h-full flex items-center justify-between pr-6 pl-4">
<div className="flex items-center space-x-4">
<div
className={`flex items-center cursor-pointer space-x-1 link-logo roll event-on`}
@ -199,20 +206,30 @@ export function TopBar({
</div>
</div>
{
isLogin ?(<div className="flex items-center space-x-4">
{isLogin ? (
<div className="flex items-center space-x-4">
{/* Pricing Link */}
<Button
variant="ghost"
size="sm"
onClick={() => {
localStorage.setItem("callBackUrl", pathname);
window.open("/pricing", "_blank");
}}
className="text-gray-300 hover:text-white"
>
Pricing
</Button>
{pathname === "/" ? (
<div
data-alt="go-started-button"
className="z-100 pointer-events-auto bg-white text-black rounded-full px-4 py-2 cursor-pointer transition-opacity opacity-100 hover:opacity-80 text-sm font-medium"
onClick={() => router.push("/movies")}
>
Go Started
</div>
) : (
<Button
variant="ghost"
size="sm"
onClick={() => {
localStorage.setItem("callBackUrl", pathname);
window.open("/pricing", "_blank");
}}
className="text-gray-300 hover:text-white"
>
Upgrade
</Button>
)}
{/* Notifications */}
{/* <Button variant="ghost" size="sm" onClick={() => showQueueNotification(3, 10)}>
@ -244,8 +261,13 @@ export function TopBar({
setIsOpen(!isOpen);
}}
data-alt="user-menu-trigger"
style={{
background: "unset !important",
}}
>
<User className="h-4 w-4" />
<div className="h-10 w-10 rounded-full bg-[#C73BFF] flex items-center justify-center text-white font-semibold">
{currentUser.username ? currentUser.username.charAt(0) : "MF"}
</div>
</Button>
{mounted && isOpen
@ -260,125 +282,97 @@ export function TopBar({
position: "fixed",
top: "4rem",
right: "1rem",
width: "18rem",
zIndex: 9999,
}}
className="bg-[#1E1E1E] rounded-lg shadow-lg overflow-hidden"
className="overflow-hidden rounded-xl"
data-alt="user-menu-dropdown"
onClick={(e) => e.stopPropagation()}
>
{/* User Info */}
<div className="p-4">
<div className="flex items-center space-x-3">
<div className="h-10 w-10 rounded-full bg-[#C73BFF] flex items-center justify-center text-white font-semibold">
MF
</div>
<div className="flex-1">
<p className="text-sm font-medium">
{currentUser.name || currentUser.username}
</p>
<p className="text-xs text-gray-500">
{currentUser.email}
</p>
</div>
<div
className="cursor-pointer hover:text-red-400 transition-colors duration-200"
onClick={() => {
logoutUser();
}}
title="退出登录"
>
<LogOut className="h-4 w-4" />
</div>
</div>
</div>
<UserCard plan_name={currentUser.plan_name}>
<div className="relative z-[2] w-full h-full flex flex-col text-white p-3">
{/* 顶部用户信息 */}
<div className="flex items-center space-x-3 mb-3">
<div className="h-10 w-10 rounded-full bg-[#C73BFF] flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
{currentUser.username
? currentUser.username.charAt(0)
: "MF"}
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-white truncate">
{currentUser.name || currentUser.username}
</h3>
<p className="text-xs text-gray-300 truncate">
{currentUser.email}
</p>
</div>
</div>
{/* AI Points */}
<div className="px-4 py-3 flex items-center justify-between">
<div className="flex items-center space-x-2">
<Sparkles className="h-4 w-4" />
<span className="text-white underline text-sm">
{isLoadingSubscription ? '...' : `${credits} credits`}
</span>
</div>
<Button
variant="outline"
size="sm"
className="text-white border-white hover:bg-white/10 rounded-full px-3 py-1 text-[10px]"
onClick={() => {
window.open("/pricing", "_blank");
}}
>
Upgrade
</Button>
{subscriptionStatus === 'ACTIVE' && (
<Button
variant="outline"
size="sm"
className="text-white border-white hover:bg-white/10 rounded-full px-3 py-1 text-[10px]"
onClick={handleManageSubscription}
disabled={isManagingSubscription}
>
Manage
</Button>
)}
</div>
{/* AI 积分 */}
<div className="flex items-center justify-center space-x-3 mb-4">
<div className="p-2 rounded-full bg-white/10 backdrop-blur-sm">
<Sparkles className="h-5 w-5 text-white" />
</div>
<span className="text-white text-base font-semibold">
{isLoadingSubscription
? "Loading..."
: `${credits} credits`}
</span>
</div>
{/* Menu Items */}
<div className="p-2">
{/* <motion.button
whileHover={{ backgroundColor: 'rgba(255,255,255,0.1)' }}
className="w-full flex items-center space-x-2 px-3 py-2 rounded-md text-sm text-white"
onClick={() => router.push('/my-library')}
data-alt="my-library-button"
>
<Library className="h-4 w-4" />
<span>My Library</span>
</motion.button>
{/* 操作按钮区域 */}
<div className="flex-1 flex flex-row justify-center items-end space-x-2 pb-2">
<button
className="flex-1 bg-transparent border border-white/30 text-white text-xs py-0.5 h-6 rounded hover:bg-white/10 transition-colors"
onClick={() => {
window.open("/pricing", "_blank");
}}
>
Upgrade
</button>
<motion.button
whileHover={{ backgroundColor: 'rgba(255,255,255,0.1)' }}
className="w-full flex items-center space-x-2 px-3 py-2 rounded-md text-sm text-white"
onClick={() => {
// 处理退出登录
setIsOpen(false);
}}
data-alt="logout-button"
>
<LogOut className="h-4 w-4" />
<span>Logout</span>
</motion.button> */}
{currentUser.plan_name !== "none" && (
<button
className="flex-1 bg-transparent border border-gray-400/30 text-gray-300 text-xs py-0.5 h-6 rounded hover:bg-gray-400/10 transition-colors disabled:opacity-50"
onClick={handleManageSubscription}
disabled={isManagingSubscription}
>
Manage
</button>
)}
{/* Footer */}
<div className="mt-4 px-3 py-2 text-xs text-gray-400 text-center">
<div>Privacy Policy · Terms of Service</div>
<div>250819215404 | 2025/8/20 06:00:50</div>
</div>
</div>
<button
className="flex-1 bg-transparent border border-red-400/50 text-red-300 text-xs py-0.5 h-6 rounded hover:bg-red-400/10 transition-colors"
onClick={() => logoutUser()}
>
Logout
</button>
</div>
</div>
</UserCard>
</motion.div>,
document.body
)
: null}
</div>
</div>):(
<div className="flex items-center space-x-4">
<button
data-alt="login-button"
className="w-[8.5rem] h-[3rem] text-base text-gray-300 hover:text-white transition-colors"
onClick={() => router.push("/login")}
>
Login
</button>
<button
data-alt="signup-button"
className="w-[8.5rem] h-[3rem] text-base bg-gray-200 text-gray-800 rounded-full hover:bg-white transition-colors"
onClick={() => router.push("/signup")}
>
Sign Up
</button>
</div>
) : (
<div className="flex items-center space-x-4">
<div
data-alt="login-button"
className="z-100 pointer-events-auto text-gray-300 hover:text-white cursor-pointer px-3 py-2 rounded transition-colors text-sm"
onClick={() => router.push("/signup")}
>
Sign Up
</div>
)
}
<div
data-alt="go-started-button"
className="z-100 pointer-events-auto bg-white text-black rounded-full px-4 py-2 cursor-pointer transition-opacity opacity-100 hover:opacity-80 text-sm font-medium"
onClick={() => router.push("/movies")}
>
Go Started
</div>
</div>
)}
</div>
</div>
);

View File

@ -106,9 +106,7 @@ export default function CreateToVideo2() {
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="flex items-center gap-2 rounded-full
bg-white/10 border border-white/20
px-3 py-1 backdrop-blur-md shadow-[0_0_8px_rgba(255,255,255,0.3)]"
className="flex items-center"
>
{/* 进行中 脉冲小圆点 */}
{status === 'pending' && (
@ -171,7 +169,7 @@ export default function CreateToVideo2() {
<div
key={project.project_id}
className="group flex flex-col bg-black/20 rounded-lg overflow-hidden cursor-pointer"
onClick={() => router.push(`/create/work-flow?episodeId=${project.project_id}`)}
onClick={() => router.push(`/movies/work-flow?episodeId=${project.project_id}`)}
onMouseEnter={() => handleMouseEnter(project.project_id)}
onMouseLeave={() => handleMouseLeave(project.project_id)}
data-alt="project-card"
@ -208,7 +206,7 @@ export default function CreateToVideo2() {
<div className="p-4">
<div className="flex items-center justify-between">
<h2 className="text-sm font-medium text-white line-clamp-1">
{project.name || "Unnamed"}
{/* {project.name || "Unnamed"} */}
</h2>
</div>
</div>
@ -217,92 +215,51 @@ export default function CreateToVideo2() {
};
return (
<>
<div className="flex flex-col absolute top-[5rem] left-0 right-0 bottom-[1rem] px-6">
{/* 优化后的主要内容区域 */}
<div className="flex-1 min-h-0">
<div
ref={scrollContainerRef}
className="h-full overflow-y-auto overflow-x-hidden custom-scrollbar"
style={{
scrollbarWidth: 'thin',
scrollbarColor: 'rgba(255,255,255,0.1) transparent'
}}
>
{isLoading && episodeList.length === 0 ? (
/* 优化的加载状态 */
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 pb-6">
{[...Array(6)].map((_, index) => (
<div
key={index}
className="group relative aspect-video bg-black/20 rounded-lg overflow-hidden animate-pulse"
>
{/* 背景占位 */}
<div className="w-full h-full bg-gradient-to-br from-white/[0.04] to-white/[0.02]" />
{/* 渐变遮罩 */}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" />
<div className="flex flex-col w-full h-full relative px-2">
<div
ref={scrollContainerRef}
className="h-full overflow-y-auto overflow-x-hidden custom-scrollbar"
style={{
scrollbarWidth: 'thin',
scrollbarColor: 'rgba(255,255,255,0.1) transparent'
}}
>
{episodeList.length > 0 && (
/* 优化的剧集网格 */
<div className="pb-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{episodeList.map(renderProjectCard)}
</div>
{/* 状态标签占位 */}
<div className="absolute top-3 left-3">
<div className="flex items-center gap-2 rounded-full bg-white/10 px-3 py-1">
<div className="w-2 h-2 rounded-full bg-white/20"></div>
<div className="w-16 h-3 bg-white/20 rounded-full"></div>
</div>
</div>
{/* 项目ID占位 */}
<div className="absolute top-3 right-3">
<div className="w-20 h-3 bg-white/10 rounded-full"></div>
</div>
{/* 底部信息占位 */}
<div className="absolute bottom-0 left-0 right-0 p-4">
<div className="w-2/3 h-5 bg-white/10 rounded-lg"></div>
</div>
</div>
))}
</div>
) : episodeList.length > 0 ? (
/* 优化的剧集网格 */
<div className="pb-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{episodeList.map(renderProjectCard)}
{/* 加载更多指示器 */}
{isLoadingMore && (
<div className="flex justify-center py-12">
<div className="flex items-center gap-3 px-6 py-3 bg-black/30 backdrop-blur-xl border border-white/10 rounded-full">
<Loader2 className="w-5 h-5 animate-spin text-purple-400" />
<span className="text-white/90 font-medium">Loading more projects...</span>
</div>
</div>
)}
{/* 到底提示 */}
{!hasMore && episodeList.length > 0 && (
<div className="flex justify-center py-12">
<div className="text-center">
<div className="w-12 h-12 bg-black/30 backdrop-blur-xl border border-white/10 rounded-full flex items-center justify-center mx-auto mb-3">
<Check className="w-6 h-6 text-purple-400" />
</div>
<p className="text-white/70 text-sm">All projects loaded</p>
</div>
{/* 加载更多指示器 */}
{isLoadingMore && (
<div className="flex justify-center py-12">
<div className="flex items-center gap-3 px-6 py-3 bg-black/30 backdrop-blur-xl border border-white/10 rounded-full">
<Loader2 className="w-5 h-5 animate-spin text-purple-400" />
<span className="text-white/90 font-medium">...</span>
</div>
</div>
)}
{/* 到底提示 */}
{!hasMore && episodeList.length > 0 && (
<div className="flex justify-center py-12">
<div className="text-center">
<div className="w-12 h-12 bg-black/30 backdrop-blur-xl border border-white/10 rounded-full flex items-center justify-center mx-auto mb-3">
<Check className="w-6 h-6 text-purple-400" />
</div>
<p className="text-white/70 text-sm"></p>
</div>
</div>
)}
</div>
) : (
<></>
)}
</div>
</div>
)}
</div>
{/* 视频工具组件 - 使用独立组件 */}
{!isLoading &&
<ChatInputBox noData={episodeList.length === 0} />
}
</>
</div>
);
}

View File

@ -216,7 +216,7 @@ function HomeModule1() {
return (
<div className="home-module1 relative flex justify-center items-start pt-[28rem] w-full h-[1280px] bg-black snap-start">
<video
src="https://cdn.qikongjian.com/videos/home.mp4"
src="https://cdn.qikongjian.com/1756549479451_ltrtoz.mp4"
autoPlay
loop
muted
@ -225,7 +225,7 @@ function HomeModule1() {
></video>
<div className="center z-10 flex flex-col items-center">
<h1 className="text-white text-[5.75rem] leading-[100%] font-bold mb-[1rem]">
Ideas Become Movies
Ideas Spark Movies
</h1>
<p className="text-white text-[2rem] leading-[140%] font-normal">
One line, one filmyour story, your scene.
@ -235,10 +235,15 @@ function HomeModule1() {
</p>
<div
className="w-[11.5rem] h-[3.75rem] mt-[4rem] text-base flex justify-center items-center font-normal border border-white rounded-full bg-white/30 cursor-pointer"
onClick={() => router.push("/create")}
onClick={() => {
if (localStorage.getItem("token")) {
router.push("/movies");
} else {
router.push("/login");
}
}}
>
Early Access
<CircleArrowRight className="ml-[1rem]" />
Make a Movie
</div>
</div>
</div>
@ -249,21 +254,21 @@ function HomeModule2() {
const videoList = [
{
title: "Text to Movie",
video: "https://cdn.qikongjian.com/videos/module2 (1).mp4",
video: "https://cdn.qikongjian.com/1756559467841_kn9fr9.mp4",
},
{
title: "Image to Movie",
video: "https://cdn.qikongjian.com/videos/module2 (2).mp4",
video: "https://cdn.qikongjian.com/1756559467840_m4ijd7.mp4",
},
{
title: "Template to Movie",
video: "https://cdn.qikongjian.com/videos/module2 (3).mp4",
video: "https://cdn.qikongjian.com/1756559467836_ij9y54.mp4",
},
];
return (
<div
data-alt="core-value-section"
className="home-module2 relative flex flex-col items-center justify-center w-full h-[1300px] bg-black snap-start"
className="home-module2 relative flex flex-col items-center justify-center w-full h-[1000px] bg-black snap-start"
>
<div
data-alt="core-value-content"
@ -272,7 +277,7 @@ function HomeModule2() {
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal mb-[3rem]">
Just Drop A Thought
</h2>
<p className="text-white text-[1.125rem] leading-[140%] font-normal text-center">
<p className="text-white text-[1.7rem] leading-[140%] font-normal text-center">
Say your idea in a single line,and MovieFlow will bring it to life.
</p>
</div>
@ -289,11 +294,14 @@ function HomeModule2() {
>
<video
src={item.video}
autoPlay
loop
muted
playsInline
className=" h-[20rem] object-cover border border-white/20 rounded-lg"
onMouseEnter={(e) => {
const videoElement = e.currentTarget;
videoElement.play();
}}
className=" h-[20rem] object-cover border border-white/20 rounded-2xl"
/>
<h3 className="mt-[1rem] text-white text-[1.5rem] font-medium">
{item.title}
@ -308,25 +316,28 @@ function HomeModule2() {
function HomeModule3() {
const videoList = [
[
"https://cdn.qikongjian.com/videos/show (1).mp4",
"https://cdn.qikongjian.com/videos/show (2).mp4",
"https://cdn.qikongjian.com/videos/show (3).mp4",
"https://cdn.qikongjian.com/videos/show (4).mp4",
"https://cdn.qikongjian.com/videos/show (16).mp4",
"https://cdn.qikongjian.com/1756474023656_60twk5.mp4",
"https://cdn.qikongjian.com/1756474023644_14n7is.mp4",
"https://cdn.qikongjian.com/1756474023648_kocq6z.mp4",
"https://cdn.qikongjian.com/1756474023657_w10boo.mp4",
"https://cdn.qikongjian.com/1756474023657_nf8799.mp4",
"https://cdn.qikongjian.com/1756474230992_vw0ubf.mp4",
],
[
"https://cdn.qikongjian.com/1756474023655_pov4c3.mp4",
"https://cdn.qikongjian.com/1756474023663_yohi7a.mp4",
"https://cdn.qikongjian.com/1756474023661_348dx3.mp4",
"https://cdn.qikongjian.com/1756474023683_xlb34s.mp4",
"https://cdn.qikongjian.com/1756474230987_63ooji.mp4",
],
[
"https://cdn.qikongjian.com/videos/show (6).mp4",
"https://cdn.qikongjian.com/videos/show (7).mp4",
"https://cdn.qikongjian.com/videos/show (8).mp4",
"https://cdn.qikongjian.com/videos/show (9).mp4",
"https://cdn.qikongjian.com/videos/show (10).mp4",
],
[
"https://cdn.qikongjian.com/videos/show (11).mp4",
"https://cdn.qikongjian.com/videos/show (12).mp4",
"https://cdn.qikongjian.com/videos/show (13).mp4",
"https://cdn.qikongjian.com/videos/show (14).mp4",
"https://cdn.qikongjian.com/videos/show (15).mp4",
"https://cdn.qikongjian.com/1756474230997_zysje8.mp4",
"https://cdn.qikongjian.com/1756474230988_tgqzln.mp4",
"https://cdn.qikongjian.com/1756474231007_qneeia.mp4",
"https://cdn.qikongjian.com/1756474231008_qyqtka.mp4",
"https://cdn.qikongjian.com/1756474231009_vs49d9.mp4",
"https://cdn.qikongjian.com/1756474231010_2a48p0.mp4",
],
];
@ -344,7 +355,7 @@ function HomeModule3() {
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal mb-[3rem]">
Ideas Made Real
</h2>
<p className="text-white text-[1.125rem] leading-[140%] font-normal text-center">
<p className="text-white text-[1.7rem] leading-[140%] font-normal text-center">
High-quality films, any style, made with MovieFlow.
</p>
</div>
@ -436,25 +447,25 @@ function HomeModule4() {
const [activeTab, setActiveTab] = useState(0);
const processSteps = [
{
title:" The Story Agent",
title: " The Story Agent",
description:
" From a single thought, it builds entire worlds and compelling plots.",
video: "https://cdn.qikongjian.com/videos/module4 (3).mp4",
},
{
title:" AI Character Agent",
title: " AI Character Agent",
description:
"Cast your virtual actors. Lock them in once, for the entire story.",
video: "https://cdn.qikongjian.com/videos/module4 (1).mp4",
},
{
title:" The Shot Agent",
title: " The Shot Agent",
description:
"It translates your aesthetic into art, light, and cinematography for every single shot.",
video: "https://cdn.qikongjian.com/videos/module4 (4).mp4",
},
{
title:" Intelligent Clip Agent",
title: " Intelligent Clip Agent",
description:
"An editing AI drives the final cut, for a story told seamlessly.",
video: "https://cdn.qikongjian.com/videos/module4 (2).mp4",
@ -468,11 +479,11 @@ function HomeModule4() {
return (
<div
data-alt="core-value-section"
className="home-module4 h-[1280px] relative flex flex-col items-center justify-center w-full bg-black snap-start"
className="home-module4 h-[1000px] relative flex flex-col items-center justify-center w-full bg-black snap-start"
>
<div
data-alt="core-value-content"
className="center z-10 flex flex-col items-center mb-[14rem]"
className="center z-10 flex flex-col items-center mb-[10rem]"
>
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal ">
Create Your Way
@ -486,7 +497,7 @@ function HomeModule4() {
<div
key={index}
onClick={() => handleTabClick(index)}
className={`w-[31.75rem] h-[10.5rem] rounded-lg cursor-pointer transition-all duration-300 border ${
className={`w-[31.75rem] h-[10.5rem] rounded-2xl cursor-pointer transition-all duration-300 border ${
activeTab === index
? "bg-[#262626] border-white/20 hover:border-white/40"
: "bg-black border-white/10 hover:border-white/40"
@ -514,7 +525,7 @@ function HomeModule4() {
{/* 右侧视频播放区域 */}
<div className="flex-1 flex justify-center">
<div className="w-[80rem] h-[45rem] bg-gray-800 rounded-lg overflow-hidden border border-white/20">
<div className="w-[80rem] h-[45rem] bg-gray-800 rounded-2xl overflow-hidden border border-white/20">
<video
key={activeTab}
src={processSteps[activeTab].video}
@ -558,6 +569,7 @@ function HomeModule5() {
credits: string;
buttonText: string;
features: string[];
issubscribed: boolean;
}[]
>(() => {
return plans.map((plan) => {
@ -569,15 +581,13 @@ function HomeModule5() {
: plan.price_year / 100,
credits: plan.description,
buttonText: plan.is_free ? "Try For Free" : "Subscribe Now",
issubscribed: plan.is_subscribed,
features: plan.features || [],
};
});
}, [plans, billingType]);
const handleSubscribe = async (planName: string) => {
if (planName === "hobby") {
return;
}
localStorage.setItem("callBackUrl", pathname);
try {
// 使用新的Checkout Session方案更简单
@ -616,10 +626,10 @@ function HomeModule5() {
>
<div
data-alt="core-value-content"
className="center z-10 flex flex-col items-center mb-[8rem]"
className="center z-10 flex flex-col items-center mb-[4rem]"
>
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal mb-[1.5rem]">
Pick a plan and make it yours
Pick a plan and make it yours
</h2>
{/* 计费切换 */}
@ -652,7 +662,7 @@ function HomeModule5() {
{pricingPlans.map((plan, index) => (
<div
key={index}
className=" w-[24rem] h-[38.125rem] bg-black rounded-lg p-[1.5rem] border border-white/20"
className=" w-[24rem] h-[38.125rem] bg-black rounded-2xl p-[1.5rem] border border-white/20"
>
<h3 className="text-white text-2xl font-normal mb-[1rem]">
{plan.title}
@ -661,17 +671,28 @@ function HomeModule5() {
<span className="text-white text-[3.375rem] font-bold">
${plan.price}
</span>
<span className="text-white text-xs ml-[0.5rem]">/month</span>
<span className="text-white text-xs ml-[0.5rem]">
/ {billingType === "month" ? "mo" : "year"}
</span>
</div>
<p className="text-white text-[0.875rem] mb-[1rem]">
{plan.credits}
</p>
<button
onClick={() => handleSubscribe(plan.title)}
className="w-full bg-white text-black py-[0.75rem] rounded-full mb-[1rem] hover:bg-black hover:text-white transition-colors border border-white/20"
>
{plan.buttonText}
</button>
{plan.issubscribed ? (
<button
disabled
className="w-full bg-gray-400 text-gray-600 py-[0.75rem] rounded-full mb-[1rem] cursor-not-allowed border border-gray-300"
>
Already Owned
</button>
) : (
<button
onClick={() => handleSubscribe(plan.title)}
className="w-full bg-white text-black py-[0.75rem] rounded-full mb-[1rem] hover:bg-black hover:text-white transition-colors border border-white/20"
>
{plan.buttonText}
</button>
)}
<p className="w-full text-center text-white/60 text-[0.75rem] mb-[2rem]">
* Billed monthly until cancelled
</p>
@ -689,8 +710,6 @@ function HomeModule5() {
</div>
))}
</div>
</div>
);
}

View File

@ -33,7 +33,7 @@ export default function Login() {
if (password.length > 18) {
return "Password cannot exceed 18 characters";
}
if (!/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d]{8,18}$/.test(password)) {
if (!/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^*&]{8,18}$/.test(password)) {
return "Password must contain both letters and numbers";
}
return "";
@ -92,13 +92,13 @@ export default function Login() {
try {
await loginUser(email, password);
// 登录成功后跳转到首页
router.push("/create");
router.push("/movies");
} catch (error: any) {
console.error("Login failed:", error);
// 根据错误类型显示不同的错误消息
setFormError(
"Login failed, please try again."
error.message || "Login failed, please try again."
);
} finally {
setIsSubmitting(false);
@ -132,7 +132,7 @@ export default function Login() {
data-alt="logo-container"
onClick={() => router.push("/")}
>
<span className="logo-heart">
<span className="logo-heart cursor-pointer">
<GradientText
text="MovieFlow"
startPercentage={30}
@ -151,7 +151,7 @@ export default function Login() {
<div className="auth-header text-center mb-4">
<h2 className="text-2xl font-bold text-white pb-2">Login</h2>
<p className="text-gray-300">
Enter your credentials to access your account
Enter your information to access your account
</p>
</div>
@ -177,7 +177,7 @@ export default function Login() {
<label className="form-label">Password</label>
<div className="relative">
<input
placeholder="8-18 characters, letters and numbers"
placeholder="8-18 characters, letters, numbers and !@#$%^*&"
required
className={`form-control pr-10 ${
passwordError ? "border-red-500/50" : ""

View File

@ -1,5 +1,5 @@
.video-tool-component {
position: fixed;
position: absolute;
bottom: 1.5rem;
z-index: 9;
}

View File

@ -26,7 +26,6 @@
.auth-container {
padding: 40px;
border-radius: 20px;
max-width: 400px;
width: 100%;
position: relative;
z-index: 10;
@ -229,7 +228,6 @@
.logo-heart {
display: inline-block;
font-size: 1.5rem;
animation: heartbeat 1.5s ease-in-out infinite;
transform-origin: center;
margin: 0 0.2rem;
}
@ -498,4 +496,4 @@
top: max(1rem, calc(env(safe-area-inset-top) + 0.5rem));
left: max(1rem, calc(env(safe-area-inset-left) + 0.5rem));
}
}
}

View File

@ -3,19 +3,18 @@ import React, { useRef, useEffect, useCallback } from "react";
import "./style/work-flow.css";
import { Skeleton } from "@/components/ui/skeleton";
import { EditModal } from "@/components/ui/edit-modal";
import { ErrorBoundary } from "@/components/ui/error-boundary";
import { TaskInfo } from "./work-flow/task-info";
import { MediaViewer } from "./work-flow/media-viewer";
import { ThumbnailGrid } from "./work-flow/thumbnail-grid";
import { useWorkflowData } from "./work-flow/use-workflow-data";
import { usePlaybackControls } from "./work-flow/use-playback-controls";
import { AlertCircle, RefreshCw, Pause, Play, ChevronLast, MessageSquareText } from "lucide-react";
import { AlertCircle, RefreshCw, Pause, Play, ChevronLast, ChevronsLeft, Bot, BriefcaseBusiness, Scissors } from "lucide-react";
import { motion } from "framer-motion";
import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { SaveEditUseCase } from "@/app/service/usecase/SaveEditUseCase";
import { useSearchParams } from "next/navigation";
import SmartChatBox from "@/components/SmartChatBox/SmartChatBox";
import { Drawer } from 'antd';
import { Drawer, Tooltip } from 'antd';
import { AIEditingIconButton } from './work-flow/ai-editing-button';
const WorkFlow = React.memo(function WorkFlow() {
@ -31,6 +30,7 @@ const WorkFlow = React.memo(function WorkFlow() {
const [previewVideoId, setPreviewVideoId] = React.useState<string | null>(null);
const [isFocusChatInput, setIsFocusChatInput] = React.useState(false);
const [aiEditingInProgress, setAiEditingInProgress] = React.useState(false);
const [isHovered, setIsHovered] = React.useState(false);
const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || '';
@ -54,7 +54,9 @@ const WorkFlow = React.memo(function WorkFlow() {
setAnyAttribute,
applyScript,
fallbackToStep,
originalText
originalText,
showGotoCutButton,
generateEditPlan
} = useWorkflowData();
const {
@ -103,125 +105,75 @@ const WorkFlow = React.memo(function WorkFlow() {
}, []);
return (
<ErrorBoundary>
<div className="w-full overflow-hidden h-[calc(100vh-5rem)] absolute top-[4rem] left-0 right-0 px-[1rem]">
<div className="w-full h-full">
<div className="splashContainer-otuV_A">
<div className="content-vPGYx8">
<div className="info-UUGkPJ">
<ErrorBoundary>
<TaskInfo
taskObject={taskObject}
currentLoadingText={currentLoadingText}
roles={taskObject.roles.data}
isPauseWorkFlow={isPauseWorkFlow}
/>
</ErrorBoundary>
</div>
<div className="w-full overflow-hidden h-full px-[1rem] pb-[1rem]">
<div className="w-full h-full">
<div className="splashContainer-otuV_A">
<div className="content-vPGYx8">
<div className="info-UUGkPJ">
<TaskInfo
taskObject={taskObject}
currentLoadingText={currentLoadingText}
roles={taskObject.roles.data}
isPauseWorkFlow={isPauseWorkFlow}
showGotoCutButton={showGotoCutButton}
onGotoCut={generateEditPlan}
setIsPauseWorkFlow={setIsPauseWorkFlow}
/>
</div>
<div className="media-Ocdu1O rounded-lg">
<div
className="videoContainer-qteKNi"
ref={containerRef}
>
{dataLoadError ? (
<motion.div
className="flex flex-col items-center justify-center w-full aspect-video rounded-lg bg-red-50 border-2 border-red-200"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<motion.div
className="flex items-center gap-3 mb-4"
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
<AlertCircle className="w-8 h-8 text-red-500" />
<h3 className="text-lg font-medium text-red-800"></h3>
</motion.div>
<p className="text-red-600 text-center mb-6 max-w-md px-4">
{dataLoadError}
</p>
<motion.button
className="flex items-center gap-2 px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
onClick={() => retryLoadData?.()}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<RefreshCw className="w-4 h-4" />
</motion.button>
</motion.div>
) : isLoading ? (
<Skeleton className="w-full aspect-video rounded-lg" />
) : (
<div className={`heroVideo-FIzuK1 ${['final_video', 'script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={taskObject.currentStage+'_'+currentSketchIndex}>
<ErrorBoundary>
<MediaViewer
taskObject={taskObject}
scriptData={scriptData}
currentSketchIndex={currentSketchIndex}
isVideoPlaying={isVideoPlaying}
onEditModalOpen={handleEditModalOpen}
onToggleVideoPlay={toggleVideoPlay}
setIsPauseWorkFlow={setIsPauseWorkFlow}
setAnyAttribute={setAnyAttribute}
isPauseWorkFlow={isPauseWorkFlow}
applyScript={applyScript}
mode={mode}
onOpenChat={() => setIsSmartChatBoxOpen(true)}
setVideoPreview={(url, id) => {
setPreviewVideoUrl(url);
setPreviewVideoId(id);
}}
/>
</ErrorBoundary>
</div>
)}
</div>
{taskObject.currentStage !== 'final_video' && taskObject.currentStage !== 'script' && (
<div className="h-[123px] w-[calc((100vh-6rem-200px)/9*16)]">
<ThumbnailGrid
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
</div>
<div className="media-Ocdu1O rounded-lg">
<div
className="videoContainer-qteKNi"
ref={containerRef}
>
{isLoading ? (
<Skeleton className="w-full aspect-video rounded-lg" />
) : (
<div className={`relative heroVideo-FIzuK1 ${['final_video', 'script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={taskObject.currentStage+'_'+currentSketchIndex}>
<MediaViewer
taskObject={taskObject}
scriptData={scriptData}
currentSketchIndex={currentSketchIndex}
onSketchSelect={setCurrentSketchIndex}
isVideoPlaying={isVideoPlaying}
onEditModalOpen={handleEditModalOpen}
onToggleVideoPlay={toggleVideoPlay}
setIsPauseWorkFlow={setIsPauseWorkFlow}
setAnyAttribute={setAnyAttribute}
isPauseWorkFlow={isPauseWorkFlow}
applyScript={applyScript}
mode={mode}
onOpenChat={() => setIsSmartChatBoxOpen(true)}
setVideoPreview={(url, id) => {
setPreviewVideoUrl(url);
setPreviewVideoId(id);
}}
showGotoCutButton={showGotoCutButton}
onGotoCut={generateEditPlan}
isSmartChatBoxOpen={isSmartChatBoxOpen}
/>
</div>
)}
</div>
{taskObject.currentStage !== 'final_video' && taskObject.currentStage !== 'script' && (
<div className="h-[123px] w-[calc((100vh-6rem-200px)/9*16)]">
<ThumbnailGrid
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
taskObject={taskObject}
currentSketchIndex={currentSketchIndex}
onSketchSelect={setCurrentSketchIndex}
/>
</div>
)}
</div>
</div>
</div>
{/* 暂停/播放按钮 */}
{
(taskObject.currentStage !== 'final_video') && (
<div className="absolute right-12 bottom-16 z-[49] flex gap-4">
<GlassIconButton
icon={isPauseWorkFlow ? Play : Pause}
size='md'
tooltip={isPauseWorkFlow ? "Play" : "Pause"}
onClick={() => setIsPauseWorkFlow(!isPauseWorkFlow)}
/>
{ !mode.includes('auto') && (
<GlassIconButton
icon={ChevronLast}
size='md'
tooltip="Next"
/>
)}
</div>
)
}
{/* AI剪辑按钮 - 当有视频片段时显示 */}
{
(taskObject.currentStage === 'video' && taskObject.videos.data.length > 0) && (
<div className="absolute right-12 bottom-48 z-[49] flex gap-4">
{/* AI剪辑按钮 - 当有视频片段时显示 */}
{
(taskObject.currentStage === 'video' && taskObject.videos.data.length > 0) && (
<div className="fixed right-[2rem] top-[8rem] z-[49]">
<Tooltip title="AI Editing" placement="left">
<AIEditingIconButton
projectId={episodeId}
taskObject={taskObject}
@ -230,81 +182,83 @@ const WorkFlow = React.memo(function WorkFlow() {
onComplete={handleAIEditingComplete}
onError={handleAIEditingError}
/>
</div>
)
}
</Tooltip>
</div>
)
}
{/* 智能对话按钮 */}
<div className="absolute right-12 bottom-32 z-[49] flex gap-4">
{/* 智能对话按钮 */}
<div
className="fixed right-[2rem] top-[4rem] z-[49]"
>
<Tooltip title="Open chat" placement="left">
<GlassIconButton
icon={MessageSquareText}
icon={Bot}
size='md'
tooltip={"Chat"}
onClick={() => setIsSmartChatBoxOpen(true)}
className="backdrop-blur-lg"
/>
</div>
{/* 智能对话弹窗 */}
<Drawer
width="25%"
placement="right"
closable={false}
maskClosable={false}
open={isSmartChatBoxOpen}
getContainer={false}
autoFocus={false}
mask={false}
zIndex={49}
rootClassName="outline-none"
className="backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl"
style={{
backgroundColor: 'transparent',
borderBottomLeftRadius: 10,
borderTopLeftRadius: 10,
overflow: 'hidden',
}}
styles={{
body: {
backgroundColor: 'transparent',
padding: 0,
},
}}
onClose={() => setIsSmartChatBoxOpen(false)}
>
<SmartChatBox
isSmartChatBoxOpen={isSmartChatBoxOpen}
setIsSmartChatBoxOpen={setIsSmartChatBoxOpen}
projectId={episodeId}
userId={userId}
previewVideoUrl={previewVideoUrl}
previewVideoId={previewVideoId}
setIsFocusChatInput={setIsFocusChatInput}
onClearPreview={() => {
setPreviewVideoUrl(null);
setPreviewVideoId(null);
}}
/>
</Drawer>
<ErrorBoundary>
<EditModal
isOpen={isEditModalOpen}
activeEditTab={activeEditTab}
onClose={() => {
SaveEditUseCase.clearData();
setIsEditModalOpen(false)
}}
taskObject={taskObject}
currentSketchIndex={currentSketchIndex}
roles={taskObject.roles.data}
setIsPauseWorkFlow={setIsPauseWorkFlow}
isPauseWorkFlow={isPauseWorkFlow}
fallbackToStep={fallbackToStep}
originalText={originalText}
/>
</ErrorBoundary>
</Tooltip>
</div>
</ErrorBoundary>
{/* 智能对话弹窗 */}
<Drawer
width="25%"
placement="right"
closable={false}
maskClosable={false}
open={isSmartChatBoxOpen}
getContainer={false}
autoFocus={false}
mask={false}
zIndex={49}
rootClassName="outline-none"
className="backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl"
style={{
backgroundColor: 'transparent',
borderBottomLeftRadius: 10,
borderTopLeftRadius: 10,
overflow: 'hidden',
}}
styles={{
body: {
backgroundColor: 'transparent',
padding: 0,
},
}}
onClose={() => setIsSmartChatBoxOpen(false)}
>
<SmartChatBox
isSmartChatBoxOpen={isSmartChatBoxOpen}
setIsSmartChatBoxOpen={setIsSmartChatBoxOpen}
projectId={episodeId}
userId={userId}
previewVideoUrl={previewVideoUrl}
previewVideoId={previewVideoId}
setIsFocusChatInput={setIsFocusChatInput}
onClearPreview={() => {
setPreviewVideoUrl(null);
setPreviewVideoId(null);
}}
/>
</Drawer>
<EditModal
isOpen={isEditModalOpen}
activeEditTab={activeEditTab}
onClose={() => {
SaveEditUseCase.clearData();
setIsEditModalOpen(false)
}}
taskObject={taskObject}
currentSketchIndex={currentSketchIndex}
roles={taskObject.roles.data}
setIsPauseWorkFlow={setIsPauseWorkFlow}
isPauseWorkFlow={isPauseWorkFlow}
fallbackToStep={fallbackToStep}
originalText={originalText}
/>
</div>
)
});

View File

@ -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 } from 'lucide-react';
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors } 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';
@ -26,6 +26,9 @@ interface MediaViewerProps {
mode: string;
onOpenChat?: () => void;
setVideoPreview?: (url: string, id: string) => void;
showGotoCutButton?: boolean;
onGotoCut: () => void;
isSmartChatBoxOpen: boolean;
}
export const MediaViewer = React.memo(function MediaViewer({
@ -41,11 +44,14 @@ export const MediaViewer = React.memo(function MediaViewer({
applyScript,
mode,
onOpenChat,
setVideoPreview
setVideoPreview,
showGotoCutButton,
onGotoCut,
isSmartChatBoxOpen
}: MediaViewerProps) {
const mainVideoRef = useRef<HTMLVideoElement>(null);
const finalVideoRef = useRef<HTMLVideoElement>(null);
const videoContentRef = useRef<HTMLDivElement>(null);
// 音量控制状态
const [isMuted, setIsMuted] = useState(false);
const [volume, setVolume] = useState(0.8);
@ -55,6 +61,17 @@ export const MediaViewer = React.memo(function MediaViewer({
const [isFullscreen, setIsFullscreen] = useState(false);
const [finalVideoReady, setFinalVideoReady] = useState(false);
const [userHasInteracted, setUserHasInteracted] = useState(false);
const [toosBtnRight, setToodsBtnRight] = useState('1rem');
useEffect(() => {
if (isSmartChatBoxOpen) {
const videoContentWidth = videoContentRef.current?.clientWidth ?? 0;
const right = (window.innerWidth * 0.25) - ((window.innerWidth - videoContentWidth) / 2) + 32;
setToodsBtnRight(right + 'px');
} else {
setToodsBtnRight('1rem');
}
}, [isSmartChatBoxOpen])
// 音量控制函数
const toggleMute = () => {
@ -300,7 +317,7 @@ export const MediaViewer = React.memo(function MediaViewer({
return (
<div
className="relative w-full h-full rounded-lg overflow-hidden"
key={`render-video-${taskObject.final.note}`}
key={`render-video-${taskObject.final.url}`}
>
<div className="relative w-full h-full group">
{/* 背景模糊的视频 */}
@ -333,7 +350,7 @@ export const MediaViewer = React.memo(function MediaViewer({
</motion.div>
{/* 操作按钮组 */}
<AnimatePresence>
{/* <AnimatePresence>
<motion.div
className="absolute top-4 right-4 z-10 gap-2 hidden group-hover:flex"
initial={{ opacity: 0, y: -10 }}
@ -343,36 +360,11 @@ export const MediaViewer = React.memo(function MediaViewer({
>
<GlassIconButton
icon={Edit3}
size="sm"
onClick={() => handleEditClick('3', 'final')}
/>
</motion.div>
</AnimatePresence>
{/* 视频信息浮层 */}
<motion.div
className="absolute bottom-0 left-0 right-0 z-10 p-4 bg-gradient-to-t from-black/80 via-black/40 to-transparent"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1, duration: 0.6 }}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<motion.div
className="w-2 h-2 rounded-full bg-emerald-500"
animate={{
scale: [1, 1.2, 1],
opacity: [1, 0.6, 1]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
<span className="text-sm font-medium text-white/90">{taskObject.final.note === 'final' ? 'Final product' : 'Trailer Video'}</span>
</div>
</div>
</motion.div>
</AnimatePresence> */}
{/* 底部控制区域 */}
<motion.div
@ -404,33 +396,23 @@ export const MediaViewer = React.memo(function MediaViewer({
size="sm"
/>
</motion.div>
{/* 完成标记 */}
<motion.div
className="absolute top-4 right-4 px-3 py-1.5 rounded-full bg-emerald-500/20 backdrop-blur-sm
border border-emerald-500/30 text-emerald-400 text-sm font-medium"
initial={{ opacity: 0, scale: 0.8, x: 20 }}
animate={{ opacity: 1, scale: 1, x: 0 }}
transition={{ delay: 1.2, duration: 0.6 }}
>
Task completed
</motion.div>
</div>
</div>
);
};
// 渲染视频内容
const renderVideoContent = () => {
const urls = taskObject.videos.data[currentSketchIndex].urls ? taskObject.videos.data[currentSketchIndex].urls.join(',') : '';
const renderVideoContent = (onGotoCut: () => void) => {
const urls = taskObject.videos.data[currentSketchIndex]?.urls ? taskObject.videos.data[currentSketchIndex]?.urls.join(',') : '';
return (
<div
className="relative w-full h-full rounded-lg group"
key={`render-video-${urls}`}
ref={videoContentRef}
>
{/* 背景模糊的图片 */}
{taskObject.videos.data[currentSketchIndex].video_status !== 1 && (
<div className="absolute inset-0 overflow-hidden z-20" style={{background: `url(${taskObject.shot_sketch.data[currentSketchIndex]?.url}) no-repeat center center`}}>
<div className="absolute inset-0 overflow-hidden z-20">
{/* 生成中 */}
{taskObject.videos.data[currentSketchIndex].video_status === 0 && (
<div className="absolute inset-0 bg-black/10 flex items-center justify-center">
@ -451,7 +433,7 @@ export const MediaViewer = React.memo(function MediaViewer({
)}
</div>
)}
{/* 视频 多个 取第一个 */}
{ taskObject.videos.data[currentSketchIndex].urls && (
@ -479,27 +461,31 @@ export const MediaViewer = React.memo(function MediaViewer({
/>
</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={() => {
{/* 跳转剪辑按钮 */}
<div className="absolute top-4 right-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
right: toosBtnRight
}}>
{/* 添加到chat去编辑 按钮 */}
<Tooltip placement="top" title="Edit video with chat">
<GlassIconButton icon={Video} size='sm' text="Edit with chat" 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>
}} />
</Tooltip>
{showGotoCutButton && (
<Tooltip placement="top" title='Go to AI-powered editing platform'>
<GlassIconButton icon={Scissors} size='sm' onClick={onGotoCut} />
</Tooltip>
)}
</div>
</>
)}
{/* 操作按钮组 */}
<AnimatePresence>
{/* <AnimatePresence>
<motion.div
className="absolute top-4 right-4 gap-2 z-[21] hidden group-hover:flex"
initial={{ opacity: 0, y: -10 }}
@ -512,7 +498,7 @@ export const MediaViewer = React.memo(function MediaViewer({
onClick={() => handleEditClick('3')}
/>
</motion.div>
</AnimatePresence>
</AnimatePresence> */}
{/* 底部控制区域 */}
{ taskObject.videos.data[currentSketchIndex].video_status === 1 && (
@ -598,7 +584,7 @@ export const MediaViewer = React.memo(function MediaViewer({
</div>
{/* 操作按钮组 */}
<AnimatePresence>
{/* <AnimatePresence>
<motion.div
className="absolute top-4 right-4 gap-2 hidden group-hover:flex"
initial={{ opacity: 0, y: -10 }}
@ -611,7 +597,7 @@ export const MediaViewer = React.memo(function MediaViewer({
onClick={() => handleEditClick('1')}
/>
</motion.div>
</AnimatePresence>
</AnimatePresence> */}
{/* 底部播放按钮 */}
<AnimatePresence>
@ -627,7 +613,7 @@ export const MediaViewer = React.memo(function MediaViewer({
whileTap={{ scale: 0.9 }}
className="relative"
>
</motion.div>
</motion.div>
</AnimatePresence>
@ -672,7 +658,7 @@ export const MediaViewer = React.memo(function MediaViewer({
}
if (taskObject.currentStage === 'video') {
return renderVideoContent();
return renderVideoContent(onGotoCut);
}
if (taskObject.currentStage === 'script') {
@ -683,9 +669,7 @@ export const MediaViewer = React.memo(function MediaViewer({
return renderSketchContent([...taskObject.roles.data, ...taskObject.scenes.data][currentSketchIndex]);
}
if (taskObject.currentStage === 'shot_sketch') {
return renderSketchContent(taskObject.shot_sketch.data[currentSketchIndex]);
}
return null;
});

View File

@ -8,15 +8,22 @@ import {
Heart,
Camera,
Film,
Scissors
Scissors,
Play,
Pause
} from 'lucide-react';
import { TaskObject } from '@/api/DTO/movieEdit';
import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { Tooltip } from 'antd';
interface TaskInfoProps {
taskObject: TaskObject;
currentLoadingText: string;
roles: any[];
isPauseWorkFlow: boolean;
showGotoCutButton: boolean;
onGotoCut?: () => void;
setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void;
}
const stageIconMap = {
@ -38,10 +45,11 @@ const stageIconMap = {
}
}
const TAG_COLORS = ['#126821', '#A133FF', '#3333FF', '#a1115e'];
const TAG_COLORS = ['#924eadcc', '#4c90a0', '#3b4a5a', '#957558'];
// const TAG_COLORS = ['#6bf5f9', '#92a6fc', '#ac71fd', '#c73dfe'];
// 阶段图标组件
const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow }: { currentStage: number, isExpanded: boolean, isPauseWorkFlow: boolean }) => {
const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow, setIsPauseWorkFlow }: { currentStage: number, isExpanded: boolean, isPauseWorkFlow: boolean, setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void }) => {
// 根据当前阶段重新排序图标
const orderedStages = useMemo(() => {
const stages = Object.entries(stageIconMap).map(([stage, data]) => ({
@ -53,69 +61,73 @@ const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow }: { currentStag
}, [currentStage]);
return (
<motion.div
className="relative flex items-center"
>
<AnimatePresence mode="popLayout">
{orderedStages.map((stage, index) => {
const isCurrentStage = stage.stage === currentStage;
const Icon = stage.icon;
// 只显示当前阶段或展开状态
if (!isExpanded && !isCurrentStage) return null;
return (
<motion.div
key={stage.stage}
className="relative"
initial={isExpanded ? {
opacity: 0,
x: -20,
scale: 0.5
} : {}}
animate={{
opacity: 1,
x: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 25,
delay: index * 0.1
}
}}
exit={{
opacity: 0,
x: 20,
scale: 0.5,
transition: { duration: 0.2 }
}}
style={{
marginLeft: index > 0 ? '8px' : '0px',
zIndex: isCurrentStage ? 2 : 1
}}
>
<Tooltip title={isPauseWorkFlow ? "Click to Play" : "Click to Pause"} placement="bottom">
<motion.div
className="relative flex items-center cursor-pointer"
onClick={() => setIsPauseWorkFlow(!isPauseWorkFlow)}
>
<AnimatePresence mode="popLayout">
{orderedStages.map((stage, index) => {
const isCurrentStage = stage.stage === currentStage;
const Icon = stage.icon;
// 只显示当前阶段或展开状态
if (!isExpanded && !isCurrentStage) return null;
return (
<motion.div
className={`relative rounded-full p-1 ${isCurrentStage ? 'bg-opacity-20' : 'bg-opacity-10'}`}
animate={(isCurrentStage && !isPauseWorkFlow) ? {
rotate: [0, 360],
scale: [1, 1.2, 1],
transition: {
rotate: { duration: 3, repeat: Infinity, ease: "linear" },
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
}
key={stage.stage}
className="relative"
initial={isExpanded ? {
opacity: 0,
x: -20,
scale: 0.5
} : {}}
animate={{
opacity: 1,
x: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 25,
delay: index * 0.1
}
}}
exit={{
opacity: 0,
x: 20,
scale: 0.5,
transition: { duration: 0.2 }
}}
style={{
marginLeft: index > 0 ? '8px' : '0px',
zIndex: isCurrentStage ? 2 : 1
}}
>
<Icon
className="w-5 h-5"
style={{ color: stage.color }}
/>
<motion.div
className={`relative rounded-full p-1 ${isCurrentStage ? 'bg-opacity-20 cursor-pointer' : 'bg-opacity-10'}`}
animate={(isCurrentStage && !isPauseWorkFlow) ? {
rotate: [0, 360],
scale: [1, 1.2, 1],
transition: {
rotate: { duration: 3, repeat: Infinity, ease: "linear" },
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
}
} : {}}
>
<Icon
className="w-5 h-5"
style={{ color: stage.color }}
/>
</motion.div>
</motion.div>
</motion.div>
);
})}
</AnimatePresence>
</motion.div>
);
})}
</AnimatePresence>
</motion.div>
</Tooltip>
);
};
@ -123,7 +135,10 @@ export function TaskInfo({
taskObject,
currentLoadingText,
roles,
isPauseWorkFlow
isPauseWorkFlow,
showGotoCutButton,
onGotoCut,
setIsPauseWorkFlow
}: TaskInfoProps) {
const [isScriptModalOpen, setIsScriptModalOpen] = useState(false);
const [currentStage, setCurrentStage] = useState(0);
@ -210,9 +225,13 @@ export function TaskInfo({
{taskObject?.tags?.map((tag: string) => (
<div
key={tag}
className="text-sm text-white rounded-full px-2 py-1"
style={{ backgroundColor: tagColors[tag] }}
data-alt="tag-item"
className="flex items-center gap-2 text-sm text-[#ececec] rounded-full px-3 py-1.5 bg-white/10 backdrop-blur-sm shadow-[0_4px_12px_rgba(0,0,0,0.2)]"
>
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: tagColors[tag] }}
/>
{tag}
</div>
))}
@ -243,13 +262,12 @@ export function TaskInfo({
</motion.div>
) : (
<motion.div
className="flex items-center gap-2 justify-center cursor-pointer"
className="flex items-center gap-2 justify-center"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
onClick={() => setIsScriptModalOpen(true)}
>
<motion.div
{/* <motion.div
className="w-1.5 h-1.5 rounded-full"
style={{ backgroundColor: stageColor }}
animate={!isPauseWorkFlow ? {
@ -261,7 +279,7 @@ export function TaskInfo({
repeatDelay: 0.2
}
} : {}}
/>
/> */}
{/* 阶段图标 */}
<motion.div
@ -274,7 +292,7 @@ export function TaskInfo({
onMouseEnter={() => setIsStageIconsExpanded(true)}
onMouseLeave={() => setIsStageIconsExpanded(false)}
>
<StageIcons currentStage={currentStage} isExpanded={isStageIconsExpanded} isPauseWorkFlow={isPauseWorkFlow} />
<StageIcons currentStage={currentStage} isExpanded={isStageIconsExpanded} isPauseWorkFlow={isPauseWorkFlow} setIsPauseWorkFlow={setIsPauseWorkFlow}/>
<motion.div
className="relative"
@ -303,7 +321,7 @@ export function TaskInfo({
{/* 主文字 - 颜色填充动画 */}
<motion.div
className="relative z-10"
className="relative z-10 cursor-pointer"
animate={!isPauseWorkFlow ? {
scale: [1, 1.02, 1],
transition: {
@ -312,6 +330,7 @@ export function TaskInfo({
ease: "easeInOut"
}
}: {}}
onClick={() => setIsScriptModalOpen(true)}
>
<motion.span
className="normalS400 subtitle-had8uE text-transparent bg-clip-text bg-gradient-to-r from-blue-600 via-cyan-500 to-purple-600"
@ -332,7 +351,7 @@ export function TaskInfo({
</motion.div>
{/* 动态光点效果 */}
<motion.div
{/* <motion.div
className="absolute left-0 top-1/2 transform -translate-y-1/2 w-2 h-2 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full blur-sm"
animate={!isPauseWorkFlow ? {
x: [0, 200, 0],
@ -344,7 +363,7 @@ export function TaskInfo({
ease: "easeInOut",
}
}: {}}
/>
/> */}
{/* 文字底部装饰线 */}
<motion.div
@ -362,7 +381,7 @@ export function TaskInfo({
</motion.div>
</motion.div>
<motion.div
{/* <motion.div
className="w-1.5 h-1.5 rounded-full"
style={{ backgroundColor: stageColor }}
animate={!isPauseWorkFlow ? {
@ -389,7 +408,14 @@ export function TaskInfo({
delay: 0.3
}
} : {}}
/>
/> */}
{/* //
{showGotoCutButton && (
<Tooltip placement="top" title='AI-powered editing platform'>
<GlassIconButton icon={Scissors} size='sm' onClick={onGotoCut} />
</Tooltip>
)} */}
</motion.div>
)}
</>

View File

@ -51,12 +51,19 @@ export function ThumbnailGrid({
if (taskObject.currentStage === 'video') {
return taskObject.videos.data;
} else if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
return [...taskObject.roles.data, ...taskObject.scenes.data];
} else if (taskObject.currentStage === 'shot_sketch') {
return taskObject.shot_sketch.data;
// 为 roles 和 scenes 数据添加唯一标识前缀,避免重复
const rolesWithPrefix = taskObject.roles.data.map((role, index) => ({
...role,
uniqueId: `role_${index}`
}));
const scenesWithPrefix = taskObject.scenes.data.map((scene, index) => ({
...scene,
uniqueId: `scene_${index}`
}));
return [...rolesWithPrefix, ...scenesWithPrefix];
}
return [];
}, [taskObject.currentStage, taskObject.videos.data, taskObject.scenes.data, taskObject.shot_sketch.data]);
}, [taskObject.currentStage, taskObject.videos.data, taskObject.roles.data, taskObject.scenes.data]);
// 使用 useRef 存储前一次的数据,避免触发重渲染
const prevDataRef = useRef<any[]>([]);
@ -162,7 +169,7 @@ export function ThumbnailGrid({
return (
<div
key={`video-${urls}`}
key={`video-${urls}-${index}`}
className={`relative aspect-video rounded-lg overflow-hidden
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
onClick={() => !isDragging && onSketchSelect(index)}
@ -195,14 +202,7 @@ export function ThumbnailGrid({
/>
) : (
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
<img
className={`w-full h-full object-cover transition-all duration-300 select-none ${
(!taskObject.shot_sketch.data[index]) ? 'filter blur-sm opacity-60' : ''
}`}
src={taskObject.shot_sketch.data[index] ? taskObject.shot_sketch.data[index].url : video.urls?.[0] || ''}
alt={`Thumbnail ${index + 1}`}
draggable="false"
/>
</div>
)}
@ -230,7 +230,7 @@ export function ThumbnailGrid({
return (
<div
key={`sketch-${sketch.url}`}
key={sketch.uniqueId || `sketch-${sketch.url}-${index}`}
className={`relative aspect-video rounded-lg overflow-hidden
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
onClick={() => !isDragging && onSketchSelect(index)}
@ -309,7 +309,6 @@ export function ThumbnailGrid({
>
{taskObject.currentStage === 'video' && renderVideoThumbnails()}
{(taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') && renderSketchThumbnails(getCurrentData())}
{taskObject.currentStage === 'shot_sketch' && renderSketchThumbnails(taskObject.shot_sketch.data)}
</div>
);
}

View File

@ -2,7 +2,7 @@
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useSearchParams } from 'next/navigation';
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan } from '@/api/video_flow';
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan, getGenerateEditPlan } from '@/api/video_flow';
import { useScriptService } from "@/app/service/Interaction/ScriptService";
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
@ -15,6 +15,9 @@ export function useWorkflowData() {
const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || '';
const from = searchParams.get('from') || '';
const token = localStorage.getItem('token') || '';
const useid = JSON.parse(localStorage.getItem("currentUser") || '{}').id || NaN;
let tempTaskObject = useRef<TaskObject>({
title: '',
@ -29,10 +32,6 @@ export function useWorkflowData() {
data: [],
total_count: -1
},
shot_sketch: {
data: [],
total_count: -1
},
videos: {
data: [],
total_count: -1
@ -52,6 +51,7 @@ export function useWorkflowData() {
const [dataLoadError, setDataLoadError] = useState<string | null>(null);
const [needStreamData, setNeedStreamData] = useState(false);
const [isPauseWorkFlow, setIsPauseWorkFlow] = useState(false);
const [canGoToCut, setCanGoToCut] = useState(false);
const [state, setState] = useState({
mode: 'automatic' as 'automatic' | 'manual' | 'auto',
originalText: '',
@ -108,11 +108,22 @@ export function useWorkflowData() {
}, []);
useEffect(() => {
if (['video', 'shot_sketch', 'sketch'].includes(taskObject.currentStage)) {
if (['video', 'sketch'].includes(taskObject.currentStage)) {
setCurrentSketchIndex(0);
}
}, [taskObject.currentStage]);
const generateEditPlan = useCallback(async () => {
// await getGenerateEditPlan({ project_id: episodeId });
window.open(`https://smartcut.huiying.video/ai-editor/${episodeId}?token=${token}&user_id=${useid}`, '_blank');
}, [episodeId]);
// useEffect(() => {
// if (!from && canGoToCut) {
// generateEditPlan();
// }
// }, [canGoToCut]);
useUpdateEffect(() => {
console.log('-----look-taskObject_find_changed-----', taskObject);
@ -143,20 +154,13 @@ export function useWorkflowData() {
loadingText.current = LOADING_TEXT_MAP.getShotSketchStatus;
}
}
if (taskObject.currentStage === 'shot_sketch') {
const realShotResultData = taskObject.shot_sketch.data.filter((item: any) => item.status !== 0);
if (taskObject.shot_sketch.total_count > realShotResultData.length) {
loadingText.current = LOADING_TEXT_MAP.shotSketch(realShotResultData.length, taskObject.shot_sketch.total_count);
} else {
loadingText.current = LOADING_TEXT_MAP.getVideoStatus;
}
}
if (taskObject.currentStage === 'video') {
const realTaskResultData = taskObject.videos.data.filter((item: any) => item.video_status !== 0);
if (taskObject.videos.total_count > realTaskResultData.length) {
loadingText.current = LOADING_TEXT_MAP.video(realTaskResultData.length, taskObject.videos.total_count);
} else {
loadingText.current = LOADING_TEXT_MAP.postProduction('generating rough cut video...');
loadingText.current = LOADING_TEXT_MAP.postProduction('AI-powered video editing in progress…');
}
}
if (taskObject.currentStage === 'final_video') {
@ -166,7 +170,7 @@ export function useWorkflowData() {
loadingText.current = LOADING_TEXT_MAP.complete;
}
setCurrentLoadingText(loadingText.current);
}, [scriptBlocksMemo, taskObject.currentStage, taskObject.scenes.data, taskObject.roles.data, taskObject.shot_sketch.data, taskObject.videos.data, taskObject.status], {mode: 'none'});
}, [scriptBlocksMemo, taskObject.currentStage, taskObject.scenes.data, taskObject.roles.data, taskObject.videos.data, taskObject.status], {mode: 'none'});
// 将 sketchCount 和 videoCount 放到 redux 中 每一次变化也要更新
@ -175,10 +179,7 @@ export function useWorkflowData() {
if (taskObject.currentStage === 'scene' && taskObject.scenes.data.length > 0) {
await autoPlaySketch(taskObject.scenes.data.length);
}
if (taskObject.currentStage === 'shot_sketch' && taskObject.shot_sketch.data.length > 0) {
await autoPlaySketch(taskObject.shot_sketch.data.length);
}
}, [taskObject.currentStage, taskObject.scenes.data, taskObject.shot_sketch.data, autoPlaySketch]);
}, [taskObject.currentStage, taskObject.scenes.data, autoPlaySketch]);
// 获取流式数据
const fetchStreamData = useCallback(async () => {
@ -198,6 +199,12 @@ export function useWorkflowData() {
// 收集所有需要更新的状态
let stateUpdates = JSON.stringify(taskCurrent);
// 视频分析
let analyze_video_completed_count = all_task_data.filter((item: any) => item.task_name === 'generate_analyze_video' && item.task_status !== 'IN_PROCESS').length;
let analyze_video_total_count = all_task_data.filter((item: any) => item.task_name === 'generate_analyze_video').length;
if (analyze_video_completed_count === analyze_video_total_count) {
setCanGoToCut(true);
}
for (const task of all_task_data) {
// 如果有已完成的数据,同步到状态
@ -256,33 +263,7 @@ export function useWorkflowData() {
}
// debugger;
if (task.task_name === 'generate_shot_sketch' && task.task_result && task.task_result.data) {
let realShotResultData = task.task_result.data.filter((item: any) => item.url);
if (task.task_status === 'COMPLETED') {
realShotResultData = taskCurrent.shot_sketch.data.filter((item: any) => item.status !== 0);
}
taskCurrent.shot_sketch.total_count = task.task_result.total_count;
if (task.task_status !== 'COMPLETED' || taskCurrent.shot_sketch.total_count !== realShotResultData.length) {
taskCurrent.currentStage = 'shot_sketch';
console.log('----------正在生成草图中 替换 sketch 数据', realShotResultData.length);
// 正在生成草图中 替换 sketch 数据
const sketchList = [];
for (const sketch of task.task_result.data) {
sketchList.push({
url: sketch.url,
script: sketch.description,
status: sketch.url ? 1 : (task.task_status === 'COMPLETED' ? 2 : 0),
type: 'shot_sketch'
});
}
taskCurrent.shot_sketch.data = sketchList;
if (task.task_status === 'COMPLETED') {
// 草图生成完成
}
break;
}
}
if (task.task_name === 'generate_videos' && task.task_result && task.task_result.data) {
let realTaskResultData = task.task_result.data.filter((item: any) => (item.urls || (item.video_status !== 0 && item.video_status !== undefined)));
@ -325,6 +306,7 @@ export function useWorkflowData() {
taskCurrent.currentStage = 'final_video';
taskCurrent.final.url = task.task_result.video;
taskCurrent.final.note = 'simple';
taskCurrent.status = 'COMPLETED';
}
}
@ -464,27 +446,7 @@ export function useWorkflowData() {
// 场景生成完成
}
}
if (data.shot_sketch && data.shot_sketch.data) {
taskCurrent.currentStage = 'shot_sketch';
const realShotResultData = data.shot_sketch.data.filter((item: any) => item.url);
const sketchList = [];
for (const sketch of data.shot_sketch.data) {
sketchList.push({
url: sketch.url,
script: sketch.description,
status: sketch.url ? 1 : (data.shot_sketch.task_status === 'COMPLETED' ? 2 : 0),
type: 'shot_sketch'
});
}
taskCurrent.shot_sketch.data = sketchList;
taskCurrent.shot_sketch.total_count = data.shot_sketch.total_count;
// 设置为最后一个草图
if (data.shot_sketch.total_count > realShotResultData.length) {
// 草图生成中
} else {
// 草图生成完成
}
}
if (data.video.data) {
const realDataVideoData = data.video.data.filter((item: any) => (item.urls || (item.video_status !== 0 && item.video_status !== undefined)));
taskCurrent.currentStage = 'video';
@ -517,12 +479,14 @@ export function useWorkflowData() {
taskCurrent.currentStage = 'final_video';
taskCurrent.final.url = data.final_simple_video.video;
taskCurrent.final.note = 'simple';
taskCurrent.status = 'COMPLETED';
}
if (data.final_video && data.final_video.video) {
taskCurrent.currentStage = 'final_video';
taskCurrent.final.url = data.final_video.video;
taskCurrent.final.note = 'final';
taskCurrent.status = 'COMPLETED';
}
}
@ -597,6 +561,9 @@ export function useWorkflowData() {
setAnyAttribute,
applyScript,
fallbackToStep,
originalText: state.originalText
originalText: state.originalText,
// showGotoCutButton: from && currentLoadingText.includes('Post-production') ? true : false,
showGotoCutButton: canGoToCut && currentLoadingText.includes('Post-production') ? true : false,
generateEditPlan
};
}

View File

@ -96,7 +96,7 @@ CharacterTabContentProps
useEffect(() => {
console.log('-==========roleData===========-', roleData);
// 只在初始化且有角色数据时执行
if (!isInitialized && roleData.length > 0) {
if (!isInitialized && (roleData?.length || 0) > 0) {
selectRole(roleData[0]);
setIsInitialized(true);
}
@ -185,7 +185,7 @@ CharacterTabContentProps
setEnableAnimation(false);
setIgnoreReplace(false);
setIsRegenerate(false);
console.log('roleData', roleData)
selectRole(roleData[index]);
};
@ -391,7 +391,7 @@ CharacterTabContentProps
</div>
</motion.div>
)}
<FloatingGlassPanel
open={isReplacePanelOpen}
@ -455,4 +455,4 @@ CharacterTabContentProps
);
});
CharacterTabContent.displayName = 'CharacterTabContent';
CharacterTabContent.displayName = 'CharacterTabContent';

View File

@ -13,6 +13,7 @@ interface GlassIconButtonProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
[key: string]: any; // To allow spreading other props
text?: string;
}
const variantStyles = {
@ -37,12 +38,12 @@ const iconSizes = {
const MotionButton = motion.button;
export const GlassIconButton = forwardRef<HTMLButtonElement, GlassIconButtonProps>(
({ icon: Icon, tooltip, variant = 'secondary', size = 'md', className, ...props }, ref) => {
({ icon: Icon, tooltip, variant = 'secondary', size = 'md', className, text, ...props }, ref) => {
return (
<MotionButton
ref={ref}
className={cn(
'relative rounded-full backdrop-blur-md transition-colors shadow-lg border',
'relative rounded-full backdrop-blur-md transition-colors shadow-lg border flex items-center gap-2',
variantStyles[variant],
sizeStyles[size],
className
@ -60,6 +61,9 @@ export const GlassIconButton = forwardRef<HTMLButtonElement, GlassIconButtonProp
{...props}
>
<Icon className={cn('text-white', iconSizes[size])} />
{text && (
<span className="text-white text-xs">{text}</span>
)}
{tooltip && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs
bg-black/80 text-white rounded-md opacity-0 group-hover:opacity-100 transition-opacity

View File

@ -48,7 +48,7 @@ export const getProjectTaskList = async (data: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage?.getItem('token') || 'mock-token'}`,
'Authorization': `Bearer ${localStorage?.getItem('token')}`,
},
body: JSON.stringify(data),
signal: controller.signal, // 添加超时控制

View File

@ -248,6 +248,19 @@ export const validateOAuthState = (state: string): boolean => {
* @returns {Promise<any>}
*/
export const getUserProfile = async (): Promise<any> => {
// const t = {
// id: '1',
// userId: '1',
// username: 'test',
// name: 'test',
// email: 'test@test.com',
// role: 'USER',
// isActive: 1,
// authType: 'email',
// lastLogin: new Date(),
// }
// setUser(t);
// return t;
try {
const token = getToken();
if (!token) {
@ -380,7 +393,7 @@ export const registerUser = async ({
const data = await response.json();
console.log('data', data)
if(!data.success){
throw new Error(data.message)
throw new Error(data.msg)
}
return data as {
success: boolean;

View File

@ -14,6 +14,7 @@ export interface SubscriptionPlan {
features: string[];
is_free: boolean;
is_popular: boolean;
is_subscribed: boolean;
sort_order: number;
}

View File

@ -49,10 +49,8 @@ const nextConfig = {
];
},
// 确保静态文件可以正常访问
experimental: {
staticPageGenerationTimeout: 120,
},
// 设置生成超时时间
staticPageGenerationTimeout: 120,
};
module.exports = nextConfig;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.