forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
eccb0a3e53
@ -23,9 +23,10 @@ interface InputBarProps {
|
|||||||
setVideoPreview?: (url: string, id: string) => void;
|
setVideoPreview?: (url: string, id: string) => void;
|
||||||
initialVideoUrl?: string;
|
initialVideoUrl?: string;
|
||||||
initialVideoId?: string;
|
initialVideoId?: string;
|
||||||
|
setIsFocusChatInput?: (v: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVideoId }: InputBarProps) {
|
export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVideoId, setIsFocusChatInput }: InputBarProps) {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
@ -91,6 +92,22 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
|
|||||||
debouncedAdjustHeight();
|
debouncedAdjustHeight();
|
||||||
}, [text, debouncedAdjustHeight]);
|
}, [text, debouncedAdjustHeight]);
|
||||||
|
|
||||||
|
// 布局切换时保持输入框焦点并将光标移到末尾
|
||||||
|
useEffect(() => {
|
||||||
|
// 等待布局动画完成后再聚焦,避免动画过程中的视觉跳动
|
||||||
|
const focusTimeout = setTimeout(() => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.focus();
|
||||||
|
// 将光标移动到文本末尾
|
||||||
|
const length = textarea.value.length;
|
||||||
|
textarea.setSelectionRange(length, length);
|
||||||
|
}
|
||||||
|
}, 200); // 与布局动画时长保持一致
|
||||||
|
|
||||||
|
return () => clearTimeout(focusTimeout);
|
||||||
|
}, [isMultiline]);
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
const blocks: MessageBlock[] = [];
|
const blocks: MessageBlock[] = [];
|
||||||
if (text.trim()) blocks.push({ type: "text" as const, text: text.trim() });
|
if (text.trim()) blocks.push({ type: "text" as const, text: text.trim() });
|
||||||
@ -229,6 +246,7 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
|
|||||||
{/* 图片上传按钮 - 单行时显示在左侧 */}
|
{/* 图片上传按钮 - 单行时显示在左侧 */}
|
||||||
{!isMultiline && (
|
{!isMultiline && (
|
||||||
<motion.label
|
<motion.label
|
||||||
|
key="single-line-upload"
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.8 }}
|
exit={{ opacity: 0, scale: 0.8 }}
|
||||||
@ -247,11 +265,11 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 文本输入 */}
|
{/* 文本输入 */}
|
||||||
<motion.div layout className="flex-1">
|
<motion.div layout key="text-input" className="flex-1 flex">
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
placeholder="输入文字…"
|
placeholder="输入文字…"
|
||||||
className="w-full pl-[10px] pr-[10px] py-[1rem] 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"
|
className="w-full pl-2 pr-2 py-4 rounded-2 leading-4 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"
|
||||||
rows={1}
|
rows={1}
|
||||||
value={text}
|
value={text}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
@ -269,6 +287,8 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
|
|||||||
handleSend();
|
handleSend();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onFocus={() => setIsFocusChatInput?.(true)}
|
||||||
|
onBlur={() => setIsFocusChatInput?.(false)}
|
||||||
data-alt="text-input"
|
data-alt="text-input"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -276,6 +296,7 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
|
|||||||
{isMultiline ? (
|
{isMultiline ? (
|
||||||
// 多行模式:底部按钮区域
|
// 多行模式:底部按钮区域
|
||||||
<motion.div
|
<motion.div
|
||||||
|
key="multi-line-buttons"
|
||||||
layout
|
layout
|
||||||
initial={{ opacity: 0, y: -10 }}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@ -284,6 +305,7 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
|
|||||||
>
|
>
|
||||||
{/* 图片上传 */}
|
{/* 图片上传 */}
|
||||||
<motion.label
|
<motion.label
|
||||||
|
key="multi-line-upload"
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className={`cursor-pointer inline-flex items-center gap-2 p-2 rounded-full hover:bg-gray-700/50 text-gray-100 ${isUploading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
className={`cursor-pointer inline-flex items-center gap-2 p-2 rounded-full hover:bg-gray-700/50 text-gray-100 ${isUploading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
@ -314,6 +336,7 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
|
|||||||
) : (
|
) : (
|
||||||
// 单行模式:发送按钮
|
// 单行模式:发送按钮
|
||||||
<motion.button
|
<motion.button
|
||||||
|
key="single-line-send"
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ interface SmartChatBoxProps {
|
|||||||
previewVideoUrl?: string | null;
|
previewVideoUrl?: string | null;
|
||||||
previewVideoId?: string | null;
|
previewVideoId?: string | null;
|
||||||
onClearPreview?: () => void;
|
onClearPreview?: () => void;
|
||||||
|
setIsFocusChatInput?: (v: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageGroup {
|
interface MessageGroup {
|
||||||
@ -42,7 +43,8 @@ export default function SmartChatBox({
|
|||||||
userId,
|
userId,
|
||||||
previewVideoUrl,
|
previewVideoUrl,
|
||||||
previewVideoId,
|
previewVideoId,
|
||||||
onClearPreview
|
onClearPreview,
|
||||||
|
setIsFocusChatInput
|
||||||
}: SmartChatBoxProps) {
|
}: SmartChatBoxProps) {
|
||||||
// 消息列表引用
|
// 消息列表引用
|
||||||
const listRef = useRef<HTMLDivElement>(null);
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
@ -197,6 +199,7 @@ export default function SmartChatBox({
|
|||||||
}}
|
}}
|
||||||
initialVideoUrl={previewVideoUrl || undefined}
|
initialVideoUrl={previewVideoUrl || undefined}
|
||||||
initialVideoId={previewVideoId || undefined}
|
initialVideoId={previewVideoId || undefined}
|
||||||
|
setIsFocusChatInput={setIsFocusChatInput}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -294,7 +294,7 @@ function transformSystemMessage(
|
|||||||
case 'generate_script_summary':
|
case 'generate_script_summary':
|
||||||
if (isScriptSummary(customData)) {
|
if (isScriptSummary(customData)) {
|
||||||
blocks = [
|
blocks = [
|
||||||
{ type: 'text', text: `🎬 剧本摘要生成完成\n\n${customData.summary}\n${content}` }
|
{ type: 'text', text: `🎬 剧本摘要生成完成\n\n${customData.summary}\n\n${content}` }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -28,6 +28,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
const [isSmartChatBoxOpen, setIsSmartChatBoxOpen] = React.useState(true);
|
const [isSmartChatBoxOpen, setIsSmartChatBoxOpen] = React.useState(true);
|
||||||
const [previewVideoUrl, setPreviewVideoUrl] = React.useState<string | null>(null);
|
const [previewVideoUrl, setPreviewVideoUrl] = React.useState<string | null>(null);
|
||||||
const [previewVideoId, setPreviewVideoId] = React.useState<string | null>(null);
|
const [previewVideoId, setPreviewVideoId] = React.useState<string | null>(null);
|
||||||
|
const [isFocusChatInput, setIsFocusChatInput] = React.useState(false);
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const episodeId = searchParams.get('episodeId') || '';
|
const episodeId = searchParams.get('episodeId') || '';
|
||||||
@ -160,7 +161,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
{taskObject.currentStage !== 'final_video' && taskObject.currentStage !== 'script' && (
|
{taskObject.currentStage !== 'final_video' && taskObject.currentStage !== 'script' && (
|
||||||
<div className="h-[123px] w-[calc((100vh-6rem-200px)/9*16)]">
|
<div className="h-[123px] w-[calc((100vh-6rem-200px)/9*16)]">
|
||||||
<ThumbnailGrid
|
<ThumbnailGrid
|
||||||
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isSmartChatBoxOpen}
|
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
|
||||||
taskObject={taskObject}
|
taskObject={taskObject}
|
||||||
currentSketchIndex={currentSketchIndex}
|
currentSketchIndex={currentSketchIndex}
|
||||||
onSketchSelect={setCurrentSketchIndex}
|
onSketchSelect={setCurrentSketchIndex}
|
||||||
@ -214,6 +215,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
mask={false}
|
mask={false}
|
||||||
zIndex={49}
|
zIndex={49}
|
||||||
|
rootClassName="outline-none"
|
||||||
className="backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl"
|
className="backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
@ -236,6 +238,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
userId={userId}
|
userId={userId}
|
||||||
previewVideoUrl={previewVideoUrl}
|
previewVideoUrl={previewVideoUrl}
|
||||||
previewVideoId={previewVideoId}
|
previewVideoId={previewVideoId}
|
||||||
|
setIsFocusChatInput={setIsFocusChatInput}
|
||||||
onClearPreview={() => {
|
onClearPreview={() => {
|
||||||
setPreviewVideoUrl(null);
|
setPreviewVideoUrl(null);
|
||||||
setPreviewVideoId(null);
|
setPreviewVideoId(null);
|
||||||
|
|||||||
@ -117,20 +117,12 @@ export function ThumbnailGrid({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 组件挂载时自动聚焦
|
// 组件挂载时自动聚焦
|
||||||
if (thumbnailsRef.current && !isDisabledFocus) {
|
if (thumbnailsRef.current && !isDisabledFocus) {
|
||||||
thumbnailsRef.current.focus();
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [handleKeyDown, isDisabledFocus]);
|
}, [handleKeyDown, isDisabledFocus]);
|
||||||
|
|
||||||
// 确保在数据变化时保持焦点
|
|
||||||
useEffect(() => {
|
|
||||||
if (thumbnailsRef.current && !isFocused && !isDisabledFocus) {
|
|
||||||
thumbnailsRef.current.focus();
|
|
||||||
}
|
|
||||||
}, [taskObject.currentStage, isFocused]);
|
|
||||||
|
|
||||||
// 处理鼠标/触摸拖动事件
|
// 处理鼠标/触摸拖动事件
|
||||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
// 阻止默认的拖拽行为
|
// 阻止默认的拖拽行为
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user