forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
61b7003622
@ -1,5 +1,5 @@
|
|||||||
import React, { useRef, useState, useEffect } from "react";
|
import React, { useRef, useState, useEffect } from "react";
|
||||||
import { Image as ImageIcon, Send, Trash2 } from "lucide-react";
|
import { Image as ImageIcon, Send, Trash2, ArrowUp } from "lucide-react";
|
||||||
import { MessageBlock } from "./types";
|
import { MessageBlock } from "./types";
|
||||||
import { useUploadFile } from "@/app/service/domain/service";
|
import { useUploadFile } from "@/app/service/domain/service";
|
||||||
|
|
||||||
@ -17,9 +17,23 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
|
|||||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||||
const [videoUrl, setVideoUrl] = useState<string | null>(initialVideoUrl || null);
|
const [videoUrl, setVideoUrl] = useState<string | null>(initialVideoUrl || null);
|
||||||
const [videoId, setVideoId] = useState<string | null>(initialVideoId || null);
|
const [videoId, setVideoId] = useState<string | null>(initialVideoId || null);
|
||||||
|
const [isMultiline, setIsMultiline] = useState(false);
|
||||||
|
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { uploadFile } = useUploadFile();
|
const { uploadFile } = useUploadFile();
|
||||||
|
|
||||||
|
const adjustHeight = () => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
const newHeight = Math.min(Math.max(textarea.scrollHeight, 48), 120);
|
||||||
|
textarea.style.height = `${newHeight}px`;
|
||||||
|
|
||||||
|
// 检查是否超过一行(48px 是单行高度)
|
||||||
|
setIsMultiline(newHeight > 48);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 监听初始视频 URL 和 ID 的变化
|
// 监听初始视频 URL 和 ID 的变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialVideoUrl && initialVideoId) {
|
if (initialVideoUrl && initialVideoId) {
|
||||||
@ -28,6 +42,11 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
|
|||||||
}
|
}
|
||||||
}, [initialVideoUrl, initialVideoId]);
|
}, [initialVideoUrl, initialVideoId]);
|
||||||
|
|
||||||
|
// 监听文本变化和组件挂载时调整高度
|
||||||
|
useEffect(() => {
|
||||||
|
adjustHeight();
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
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() });
|
||||||
@ -153,26 +172,30 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2 px-3 m-3 border border-gray-700 rounded-[2rem]">
|
<div className={`${isMultiline ? 'flex flex-col' : 'flex items-center'} gap-2 px-3 m-3 border border-gray-700 rounded-[2rem]`}>
|
||||||
{/* 图片上传 */}
|
{/* 图片上传按钮 - 单行时显示在左侧,多行时显示在底部 */}
|
||||||
<label
|
{!isMultiline && (
|
||||||
className={`cursor-pointer inline-flex items-center gap-2 p-2 my-2 rounded-full hover:bg-gray-700/50 text-gray-100 ${isUploading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
<label
|
||||||
data-alt="file-upload"
|
className={`cursor-pointer inline-flex items-center gap-2 p-2 my-2 rounded-full hover:bg-gray-700/50 text-gray-100 ${isUploading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
>
|
data-alt="file-upload"
|
||||||
<ImageIcon size={16} />
|
>
|
||||||
<input
|
<ImageIcon size={16} />
|
||||||
className="hidden"
|
<input
|
||||||
type="file"
|
className="hidden"
|
||||||
accept="image/*"
|
type="file"
|
||||||
onChange={onFileChange}
|
accept="image/*"
|
||||||
disabled={isUploading}
|
onChange={onFileChange}
|
||||||
/>
|
disabled={isUploading}
|
||||||
</label>
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 文本输入 */}
|
{/* 文本输入 */}
|
||||||
<input
|
<textarea
|
||||||
className="flex-1 bg-transparent text-gray-100 px-3 py-2 outline-none placeholder:text-gray-400"
|
ref={textareaRef}
|
||||||
placeholder="输入文字…"
|
placeholder="输入文字…"
|
||||||
|
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"
|
||||||
|
rows={1}
|
||||||
value={text}
|
value={text}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
@ -184,15 +207,45 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
|
|||||||
data-alt="text-input"
|
data-alt="text-input"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 发送按钮 */}
|
{isMultiline ? (
|
||||||
<button
|
// 多行模式:底部按钮区域
|
||||||
onClick={handleSend}
|
<div className="flex justify-between items-center pb-2">
|
||||||
className="inline-flex items-center gap-2 p-2 my-2 rounded-full bg-blue-500 hover:bg-blue-400 text-white shadow disabled:bg-gray-500 disabled:hover:bg-gray-500 disabled:cursor-not-allowed"
|
{/* 图片上传 */}
|
||||||
data-alt="send-button"
|
<label
|
||||||
disabled={!text.trim() && !imageUrl && !videoUrl}
|
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' : ''}`}
|
||||||
>
|
data-alt="file-upload"
|
||||||
<Send size={18} />
|
>
|
||||||
</button>
|
<ImageIcon size={16} />
|
||||||
|
<input
|
||||||
|
className="hidden"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={onFileChange}
|
||||||
|
disabled={isUploading}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* 发送按钮 必须有文字 */}
|
||||||
|
<button
|
||||||
|
onClick={handleSend}
|
||||||
|
className="inline-flex items-center gap-2 p-2 rounded-full bg-[#f1f3f4] text-[#25294b] shadow disabled:text-white/25 disabled:border disabled:border-white/10 disabled:bg-[#1b1b1b80] disabled:cursor-not-allowed"
|
||||||
|
data-alt="send-button"
|
||||||
|
disabled={!text.trim()}
|
||||||
|
>
|
||||||
|
<ArrowUp size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 单行模式:发送按钮
|
||||||
|
<button
|
||||||
|
onClick={handleSend}
|
||||||
|
className="inline-flex items-center gap-2 p-2 my-2 rounded-full bg-[#f1f3f4] text-[#25294b] shadow disabled:text-white/25 disabled:border disabled:border-white/10 disabled:bg-[#1b1b1b80] disabled:cursor-not-allowed"
|
||||||
|
data-alt="send-button"
|
||||||
|
disabled={!text.trim()}
|
||||||
|
>
|
||||||
|
<ArrowUp size={18} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -16,9 +16,9 @@ export function MessageRenderer({ msg }: MessageRendererProps) {
|
|||||||
const isSystem = msg.role === "system";
|
const isSystem = msg.role === "system";
|
||||||
|
|
||||||
const bubbleClass = useMemo(() => {
|
const bubbleClass = useMemo(() => {
|
||||||
if (isSystem) return "bg-amber-50 text-amber-900 border border-amber-200";
|
if (isSystem) return "bg-[#281c1459] text-white";
|
||||||
if (isUser) return "bg-blue-500/30 text-white";
|
if (isUser) return "bg-[#27416c59] text-white";
|
||||||
return "bg-[#62402733] text-gray-100"; // assistant
|
return "bg-[#281c1459] text-white"; // assistant
|
||||||
}, [isSystem, isUser]);
|
}, [isSystem, isUser]);
|
||||||
|
|
||||||
const badge = isSystem ? (
|
const badge = isSystem ? (
|
||||||
@ -78,14 +78,14 @@ export function MessageRenderer({ msg }: MessageRendererProps) {
|
|||||||
>
|
>
|
||||||
<div className={`max-w-[75%] rounded-2xl shadow-md p-3 ${bubbleClass}`}>
|
<div className={`max-w-[75%] rounded-2xl shadow-md p-3 ${bubbleClass}`}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2 text-[11px] opacity-80 mb-1">
|
{/* <div className="flex items-center gap-2 text-[11px] opacity-80 mb-1">
|
||||||
{badge}
|
{badge}
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{typeLabel}
|
{typeLabel}
|
||||||
<span>{hhmm(msg.createdAt)}</span>
|
<span>{hhmm(msg.createdAt)}</span>
|
||||||
{statusIcon}
|
{statusIcon}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Content blocks */}
|
{/* Content blocks */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -93,7 +93,7 @@ export function MessageRenderer({ msg }: MessageRendererProps) {
|
|||||||
switch (b.type) {
|
switch (b.type) {
|
||||||
case "text":
|
case "text":
|
||||||
return (
|
return (
|
||||||
<p key={idx} className="leading-relaxed whitespace-pre-wrap">
|
<p key={idx} className="leading-relaxed whitespace-pre-wrap break-words">
|
||||||
{b.text}
|
{b.text}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -167,8 +167,10 @@ export default function SmartChatBox({
|
|||||||
|
|
||||||
{/* Loading indicator */}
|
{/* Loading indicator */}
|
||||||
{isLoading && !hasMore && (
|
{isLoading && !hasMore && (
|
||||||
<div className="flex justify-center py-2">
|
<div className="flex justify-start space-x-1 p-2">
|
||||||
<div className="animate-spin rounded-full h-5 w-5 border-2 border-gray-400 border-t-white" />
|
<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>
|
||||||
|
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -286,7 +286,7 @@ function transformSystemMessage(
|
|||||||
if (isProjectInit(customData)) {
|
if (isProjectInit(customData)) {
|
||||||
blocks = [{
|
blocks = [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: `🎬 根据您输入的 "${customData.project_data.script}",我已完成项目的初始化。\n\n${content}`
|
text: `🎬 根据您输入的 "${customData.project_data.script}",我已完成项目的初始化。\n${content}`
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -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\n${content}` }
|
{ type: 'text', text: `🎬 剧本摘要生成完成\n\n${customData.summary}\n${content}` }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -317,7 +317,7 @@ function transformSystemMessage(
|
|||||||
label: `已完成 ${customData.completed_count} 个演员,共有 ${customData.total_count} 个`
|
label: `已完成 ${customData.completed_count} 个演员,共有 ${customData.total_count} 个`
|
||||||
}, {
|
}, {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: `\n\n${content}`
|
text: `\n${content}`
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -340,7 +340,7 @@ function transformSystemMessage(
|
|||||||
label: `已完成 ${customData.completed_count} 个场景,共有 ${customData.total_count} 个`
|
label: `已完成 ${customData.completed_count} 个场景,共有 ${customData.total_count} 个`
|
||||||
}, {
|
}, {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: `\n\n${content}`
|
text: `\n${content}`
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -363,7 +363,7 @@ function transformSystemMessage(
|
|||||||
label: `已完成 ${customData.completed_count} 个故事板静帧,共有 ${customData.total_count} 个`
|
label: `已完成 ${customData.completed_count} 个故事板静帧,共有 ${customData.total_count} 个`
|
||||||
}, {
|
}, {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: `\n\n${content}`
|
text: `\n${content}`
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -390,7 +390,7 @@ function transformSystemMessage(
|
|||||||
label: `已完成 ${customData.completed_count} 个分镜,共有 ${customData.total_count} 个分镜`
|
label: `已完成 ${customData.completed_count} 个分镜,共有 ${customData.total_count} 个分镜`
|
||||||
}, {
|
}, {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: `\n\n${content}`
|
text: `\n${content}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -205,7 +205,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
|
|
||||||
{/* 智能对话弹窗 */}
|
{/* 智能对话弹窗 */}
|
||||||
<Drawer
|
<Drawer
|
||||||
width="35%"
|
width="25%"
|
||||||
placement="right"
|
placement="right"
|
||||||
closable={false}
|
closable={false}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
|
|||||||
@ -30,13 +30,19 @@ export function ThumbnailGrid({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (thumbnailsRef.current) {
|
if (thumbnailsRef.current) {
|
||||||
const container = thumbnailsRef.current;
|
const container = thumbnailsRef.current;
|
||||||
const thumbnailWidth = container.offsetWidth / 4; // 每个缩略图宽度(包含间距)
|
const thumbnails = container.children;
|
||||||
const scrollPosition = currentSketchIndex * thumbnailWidth;
|
|
||||||
|
|
||||||
container.scrollTo({
|
if (currentSketchIndex >= 0 && currentSketchIndex < thumbnails.length) {
|
||||||
left: scrollPosition,
|
const thumbnail = thumbnails[currentSketchIndex] as HTMLElement;
|
||||||
behavior: 'smooth'
|
const containerLeft = container.getBoundingClientRect().left;
|
||||||
});
|
const thumbnailLeft = thumbnail.getBoundingClientRect().left;
|
||||||
|
const scrollPosition = container.scrollLeft + (thumbnailLeft - containerLeft);
|
||||||
|
|
||||||
|
container.scrollTo({
|
||||||
|
left: scrollPosition,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [currentSketchIndex]);
|
}, [currentSketchIndex]);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user