From 55b5e6e57046dd3c76086248e92164681edf544a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?=
<7854742+wang_rumeng@user.noreply.gitee.com>
Date: Sat, 23 Aug 2025 19:11:09 +0800
Subject: [PATCH] =?UTF-8?q?chatbox=E5=BC=80=E5=8F=91=E4=B8=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
api/constants.ts | 1 +
components/SmartChatBox/DateDivider.tsx | 39 ++
components/SmartChatBox/InputBar.tsx | 139 +++++
components/SmartChatBox/LoadMoreButton.tsx | 25 +
components/SmartChatBox/MessageRenderer.tsx | 126 +++++
components/SmartChatBox/ProgressBar.tsx | 26 +
components/SmartChatBox/SmartChatBox.tsx | 147 +++++
components/SmartChatBox/api.ts | 507 ++++++++++++++++++
components/SmartChatBox/types.ts | 132 +++++
components/SmartChatBox/useMessages.ts | 212 ++++++++
components/SmartChatBox/utils.ts | 17 +
components/pages/work-flow.tsx | 66 ++-
components/pages/work-flow/thumbnail-grid.tsx | 4 +-
next.config.js | 1 +
14 files changed, 1427 insertions(+), 15 deletions(-)
create mode 100644 components/SmartChatBox/DateDivider.tsx
create mode 100644 components/SmartChatBox/InputBar.tsx
create mode 100644 components/SmartChatBox/LoadMoreButton.tsx
create mode 100644 components/SmartChatBox/MessageRenderer.tsx
create mode 100644 components/SmartChatBox/ProgressBar.tsx
create mode 100644 components/SmartChatBox/SmartChatBox.tsx
create mode 100644 components/SmartChatBox/api.ts
create mode 100644 components/SmartChatBox/types.ts
create mode 100644 components/SmartChatBox/useMessages.ts
create mode 100644 components/SmartChatBox/utils.ts
diff --git a/api/constants.ts b/api/constants.ts
index 53e1866..95cd888 100644
--- a/api/constants.ts
+++ b/api/constants.ts
@@ -1,2 +1,3 @@
export const BASE_URL = process.env.NEXT_PUBLIC_SMART_API
+// export const BASE_URL ='http://192.168.120.36:8000'
//
diff --git a/components/SmartChatBox/DateDivider.tsx b/components/SmartChatBox/DateDivider.tsx
new file mode 100644
index 0000000..d6d7897
--- /dev/null
+++ b/components/SmartChatBox/DateDivider.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+
+interface DateDividerProps {
+ timestamp: number;
+}
+
+export function DateDivider({ timestamp }: DateDividerProps) {
+ const formatDate = (ts: number) => {
+ const date = new Date(ts);
+ const today = new Date();
+ const yesterday = new Date(today);
+ yesterday.setDate(yesterday.getDate() - 1);
+
+ // 判断是否是今天
+ if (date.toDateString() === today.toDateString()) {
+ return '今天';
+ }
+ // 判断是否是昨天
+ if (date.toDateString() === yesterday.toDateString()) {
+ return '昨天';
+ }
+ // 其他日期显示完整日期
+ return date.toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ });
+ };
+
+ return (
+
+
+
+ {formatDate(timestamp)}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/SmartChatBox/InputBar.tsx b/components/SmartChatBox/InputBar.tsx
new file mode 100644
index 0000000..40b358b
--- /dev/null
+++ b/components/SmartChatBox/InputBar.tsx
@@ -0,0 +1,139 @@
+import React, { useRef, useState } from "react";
+import { Image as ImageIcon, Send, Trash2 } from "lucide-react";
+import { MessageBlock } from "./types";
+import { useUploadFile } from "@/app/service/domain/service";
+
+interface InputBarProps {
+ onSend: (blocks: MessageBlock[]) => void;
+}
+
+export function InputBar({ onSend }: InputBarProps) {
+ const [text, setText] = useState("");
+ const [isUploading, setIsUploading] = useState(false);
+ const [uploadProgress, setUploadProgress] = useState(0);
+ const [imageUrl, setImageUrl] = useState(null);
+
+ const { uploadFile } = useUploadFile();
+
+ const handleSend = () => {
+ const blocks: MessageBlock[] = [];
+ if (text.trim()) blocks.push({ type: "text" as const, text: text.trim() });
+ if (imageUrl) blocks.push({ type: "image" as const, url: imageUrl });
+ if (!blocks.length) return;
+
+ onSend(blocks);
+ setText("");
+ setImageUrl(null);
+ };
+
+ const handleFileUpload = async (file: File) => {
+ try {
+ setIsUploading(true);
+ const url = await uploadFile(file, (progress) => {
+ setUploadProgress(progress);
+ });
+ setImageUrl(url);
+ } catch (error) {
+ console.error("上传失败:", error);
+ // 可以添加错误提示
+ } finally {
+ setIsUploading(false);
+ setUploadProgress(0);
+ }
+ };
+
+ const onFileChange = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ if (!file.type.startsWith('image/')) {
+ alert('请选择图片文件');
+ return;
+ }
+ await handleFileUpload(file);
+ }
+ e.currentTarget.value = ""; // reset
+ };
+
+ const removeImage = () => {
+ setImageUrl(null);
+ };
+
+ return (
+
+ {/* 图片预览 */}
+ {imageUrl && (
+
+
+

+
+
+
+ )}
+
+ {/* 上传进度 */}
+ {isUploading && (
+
+ )}
+
+
+ {/* 图片上传 */}
+
+
+ {/* 文本输入 */}
+ setText(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ }}
+ data-alt="text-input"
+ />
+
+ {/* 发送按钮 */}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/SmartChatBox/LoadMoreButton.tsx b/components/SmartChatBox/LoadMoreButton.tsx
new file mode 100644
index 0000000..dfcf1ef
--- /dev/null
+++ b/components/SmartChatBox/LoadMoreButton.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { ChevronUp } from 'lucide-react';
+
+interface LoadMoreButtonProps {
+ onClick: () => void;
+ loading?: boolean;
+}
+
+export function LoadMoreButton({ onClick, loading = false }: LoadMoreButtonProps) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/components/SmartChatBox/MessageRenderer.tsx b/components/SmartChatBox/MessageRenderer.tsx
new file mode 100644
index 0000000..f04df17
--- /dev/null
+++ b/components/SmartChatBox/MessageRenderer.tsx
@@ -0,0 +1,126 @@
+import React, { useMemo } from "react";
+import { motion } from "framer-motion";
+import { ChatMessage } from "./types";
+import { bubbleVariants, hhmm } from "./utils";
+import { ProgressBar } from "./ProgressBar";
+import { Loader2, AlertCircle, CheckCircle2 } from "lucide-react";
+import { Image } from 'antd';
+
+interface MessageRendererProps {
+ msg: ChatMessage;
+}
+
+export function MessageRenderer({ msg }: MessageRendererProps) {
+ // Decide bubble style
+ const isUser = msg.role === "user";
+ const isSystem = msg.role === "system";
+
+ const bubbleClass = useMemo(() => {
+ if (isSystem) return "bg-amber-50 text-amber-900 border border-amber-200";
+ if (isUser) return "bg-blue-500/30 text-white";
+ return "bg-gray-800 text-gray-100"; // assistant
+ }, [isSystem, isUser]);
+
+ const badge = isSystem ? (
+
+ 系统流程
+
+ ) : msg.role === "assistant" ? (
+
+ 助手
+
+ ) : null;
+
+ // 状态图标和颜色
+ const statusIcon = useMemo(() => {
+ if (!isUser) return null;
+
+ switch (msg.status) {
+ case 'pending':
+ return ;
+ case 'success':
+ return ;
+ case 'error':
+ return ;
+ default:
+ return null;
+ }
+ }, [isUser, msg.status]);
+
+ // 消息类型标签
+ const typeLabel = useMemo(() => {
+ if (!isUser) return null;
+
+ const isTask = msg.chatType !== 'chat';
+ if (isTask) {
+ return (
+
+ 任务
+
+ );
+ }
+ return (
+
+ 聊天
+
+ );
+ }, [isUser, msg.chatType]);
+
+ return (
+
+
+ {/* Header */}
+
+ {badge}
+
+ {typeLabel}
+ {hhmm(msg.createdAt)}
+ {statusIcon}
+
+
+
+ {/* Content blocks */}
+
+ {msg.blocks.map((b, idx) => {
+ switch (b.type) {
+ case "text":
+ return (
+
+ {b.text}
+
+ );
+ case "image":
+ return (
+
+
+
+ );
+ case "video":
+ return (
+
+
+
+ );
+ case "audio":
+ return (
+
+ );
+ case "progress":
+ return
;
+ default:
+ return null;
+ }
+ })}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/SmartChatBox/ProgressBar.tsx b/components/SmartChatBox/ProgressBar.tsx
new file mode 100644
index 0000000..78b7e4b
--- /dev/null
+++ b/components/SmartChatBox/ProgressBar.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+import { motion } from "framer-motion";
+
+interface ProgressBarProps {
+ value: number;
+ total?: number;
+ label?: string;
+}
+
+export function ProgressBar({ value, total = 100, label }: ProgressBarProps) {
+ const pct = Math.max(0, Math.min(100, Math.round((value / total) * 100)));
+ return (
+
+ {label ?
{label}
: null}
+
+
+
+
{pct}%
+
+ );
+}
\ No newline at end of file
diff --git a/components/SmartChatBox/SmartChatBox.tsx b/components/SmartChatBox/SmartChatBox.tsx
new file mode 100644
index 0000000..2bdfe31
--- /dev/null
+++ b/components/SmartChatBox/SmartChatBox.tsx
@@ -0,0 +1,147 @@
+import React, { useRef, useCallback } from "react";
+import { ArrowRightFromLine, ChevronDown } from 'lucide-react';
+import { Switch } from 'antd';
+import { MessageRenderer } from "./MessageRenderer";
+import { InputBar } from "./InputBar";
+import { useMessages } from "./useMessages";
+import { DateDivider } from "./DateDivider";
+import { LoadMoreButton } from "./LoadMoreButton";
+import { ChatMessage } from "./types";
+
+interface SmartChatBoxProps {
+ isSmartChatBoxOpen: boolean;
+ setIsSmartChatBoxOpen: (v: boolean) => void;
+ projectId: string;
+ userId: number;
+}
+
+interface MessageGroup {
+ date: number;
+ messages: ChatMessage[];
+}
+
+function BackToLatestButton({ onClick }: { onClick: () => void }) {
+ return (
+
+ );
+}
+
+export default function SmartChatBox({ isSmartChatBoxOpen, setIsSmartChatBoxOpen, projectId, userId }: SmartChatBoxProps) {
+ // 消息列表引用
+ const listRef = useRef(null);
+
+ // 处理消息更新时的滚动
+ const handleMessagesUpdate = useCallback((shouldScroll: boolean) => {
+ if (shouldScroll && listRef.current) {
+ listRef.current.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" });
+ }
+ }, []);
+
+ // 使用消息管理 hook
+ const [
+ { messages, isLoading, error, hasMore, loadMoreMessages, backToLatest, isViewingHistory },
+ { sendMessage },
+ { enabled: systemPush, toggle: toggleSystemPush }
+ ] = useMessages({
+ config: { projectId, userId },
+ onMessagesUpdate: handleMessagesUpdate
+ });
+
+ // 按日期分组消息
+ const groupedMessages = React.useMemo(() => {
+ const groups: MessageGroup[] = [];
+
+ messages.forEach(message => {
+ const messageDate = new Date(message.createdAt).setHours(0, 0, 0, 0);
+
+ const existingGroup = groups.find(group => {
+ const groupDate = new Date(group.date).setHours(0, 0, 0, 0);
+ return groupDate === messageDate;
+ });
+
+ if (existingGroup) {
+ existingGroup.messages.push(message);
+ } else {
+ groups.push({
+ date: messageDate,
+ messages: [message]
+ });
+ }
+ });
+
+ return groups.sort((a, b) => a.date - b.date);
+ }, [messages]);
+
+ return (
+
+ {/* Header */}
+
+
+ Chat
+ {/* System push toggle */}
+
+
+
+
setIsSmartChatBoxOpen(false)}
+ />
+
+
+
+ {/* Message list */}
+
+ {/* Load more button */}
+ {hasMore && (
+
+ )}
+
+ {/* Messages grouped by date */}
+
+ {groupedMessages.map((group) => (
+
+
+ {group.messages.map((message) => (
+
+ ))}
+
+ ))}
+
+
+ {/* Loading indicator */}
+ {isLoading && !hasMore && (
+
+ )}
+
+ {/* Error message */}
+ {error && (
+
+ {error.message}
+
+ )}
+
+ {/* Back to latest button */}
+ {isViewingHistory && (
+
+ )}
+
+
+ {/* Input */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/SmartChatBox/api.ts b/components/SmartChatBox/api.ts
new file mode 100644
index 0000000..35498f4
--- /dev/null
+++ b/components/SmartChatBox/api.ts
@@ -0,0 +1,507 @@
+import {
+ ChatMessage,
+ MessageBlock,
+ FetchMessagesRequest,
+ SendMessageRequest,
+ ChatConfig,
+ ApiResponse,
+ RealApiMessage,
+ ApiMessageContent,
+ MessagesResponse,
+ FunctionName,
+ ProjectInit,
+ ScriptSummary,
+ CharacterGeneration,
+ SketchGeneration,
+ ShotSketchGeneration
+} from "./types";
+import { post } from "@/api/request";
+
+// Mock 数据
+const MOCK_MESSAGES: RealApiMessage[] = [
+ // 用户发送剧本
+ {
+ id: 1,
+ role: 'user',
+ content: JSON.stringify([{
+ type: 'text',
+ content: '我想拍一个关于一个小女孩和她的机器人朋友的故事,故事发生在未来世界。'
+ }]),
+ created_at: '2024-03-20T10:00:00Z',
+ message_type: undefined,
+ function_name: undefined,
+ custom_data: undefined,
+ status: 'success',
+ intent_type: 'chat'
+ },
+ // 项目初始化
+ {
+ id: 2,
+ role: 'system',
+ content: '我会帮您创建一个温馨感人的科幻短片,讲述人工智能与人类情感的故事。',
+ created_at: '2024-03-20T10:00:10Z',
+ message_type: 'project_init',
+ function_name: 'create_project',
+ custom_data: {
+ project_data: {
+ script: '小女孩和机器人朋友的故事'
+ }
+ },
+ status: 'success',
+ intent_type: 'procedure'
+ },
+ // 剧本总结
+ {
+ id: 3,
+ role: 'system',
+ content: '故事概要:在2045年的未来城市,10岁的小女孩艾米丽收到了一个特别的生日礼物——一个具有高度情感智能的机器人伙伴"小星"。随着时间推移,他们建立了深厚的友谊。当小星因能源耗尽即将永久关闭时,艾米丽想尽办法寻找解决方案,最终通过她的坚持和创意,成功为小星找到了新的能源,让这段跨越人机界限的友谊得以延续。',
+ created_at: '2024-03-20T10:01:00Z',
+ message_type: 'script_summary',
+ function_name: 'generate_script_summary',
+ custom_data: {
+ summary: '一个关于友谊和希望的温暖故事'
+ },
+ status: 'success',
+ intent_type: 'procedure'
+ },
+ // 角色生成 - 艾米丽
+ {
+ id: 4,
+ role: 'system',
+ content: '主角艾米丽的形象已生成',
+ created_at: '2024-03-20T10:02:00Z',
+ message_type: 'character_generation',
+ function_name: 'generate_character',
+ custom_data: {
+ character_name: '艾米丽',
+ image_path: 'https://picsum.photos/seed/emily/300/400',
+ count: 1,
+ total_count: 2
+ },
+ status: 'success',
+ intent_type: 'procedure'
+ },
+ // 角色生成 - 小星
+ {
+ id: 5,
+ role: 'system',
+ content: '机器人小星的形象已生成',
+ created_at: '2024-03-20T10:03:00Z',
+ message_type: 'character_generation',
+ function_name: 'generate_character',
+ custom_data: {
+ character_name: '小星',
+ image_path: 'https://picsum.photos/seed/robot/300/400',
+ count: 2,
+ total_count: 2
+ },
+ status: 'success',
+ intent_type: 'procedure'
+ },
+ // 场景生成 - 未来城市
+ {
+ id: 6,
+ role: 'system',
+ content: '未来城市场景设计完成',
+ created_at: '2024-03-20T10:04:00Z',
+ message_type: 'sketch_generation',
+ function_name: 'generate_sketch',
+ custom_data: {
+ sketch_name: '未来城市街景',
+ image_path: 'https://picsum.photos/seed/city/600/400',
+ count: 1,
+ total_count: 3
+ },
+ status: 'success',
+ intent_type: 'procedure'
+ },
+ // 场景生成 - 艾米丽的房间
+ {
+ id: 7,
+ role: 'system',
+ content: '艾米丽的未来风格卧室设计完成',
+ created_at: '2024-03-20T10:05:00Z',
+ message_type: 'sketch_generation',
+ function_name: 'generate_sketch',
+ custom_data: {
+ sketch_name: '艾米丽的卧室',
+ image_path: 'https://picsum.photos/seed/room/600/400',
+ count: 2,
+ total_count: 3
+ },
+ status: 'success',
+ intent_type: 'procedure'
+ },
+ // 场景生成 - 实验室
+ {
+ id: 8,
+ role: 'system',
+ content: '高科技实验室场景设计完成',
+ created_at: '2024-03-20T10:06:00Z',
+ message_type: 'sketch_generation',
+ function_name: 'generate_sketch',
+ custom_data: {
+ sketch_name: '未来实验室',
+ image_path: 'https://picsum.photos/seed/lab/600/400',
+ count: 3,
+ total_count: 3
+ },
+ status: 'success',
+ intent_type: 'procedure'
+ },
+ // 分镜生成 - 相遇
+ {
+ id: 9,
+ role: 'system',
+ content: '第一个分镜:艾米丽收到礼物时的场景',
+ created_at: '2024-03-20T10:07:00Z',
+ message_type: 'shot_sketch_generation',
+ function_name: 'generate_shot_sketch',
+ custom_data: {
+ shot_type: '中景',
+ atmosphere: '温馨、期待',
+ key_action: '艾米丽惊喜地打开礼物盒,小星缓缓启动',
+ url: 'https://picsum.photos/seed/shot1/600/400',
+ count: 1,
+ total_count: 3
+ },
+ status: 'success',
+ intent_type: 'procedure'
+ },
+ // 分镜生成 - 危机
+ {
+ id: 10,
+ role: 'system',
+ content: '第二个分镜:小星能源耗尽的场景',
+ created_at: '2024-03-20T10:08:00Z',
+ message_type: 'shot_sketch_generation',
+ function_name: 'generate_shot_sketch',
+ custom_data: {
+ shot_type: '特写',
+ atmosphere: '紧张、担忧',
+ key_action: '小星的能源指示灯闪烁微弱,艾米丽神情焦急',
+ url: 'https://picsum.photos/seed/shot2/600/400',
+ count: 2,
+ total_count: 3
+ },
+ status: 'success',
+ intent_type: 'procedure'
+ },
+ // 分镜生成 - 解决
+ {
+ id: 11,
+ role: 'system',
+ content: '第三个分镜:找到新能源解决方案的场景',
+ created_at: '2024-03-20T10:09:00Z',
+ message_type: 'shot_sketch_generation',
+ function_name: 'generate_shot_sketch',
+ custom_data: {
+ shot_type: '全景',
+ atmosphere: '欢欣、胜利',
+ key_action: '实验室中艾米丽成功激活新能源,小星重新焕发活力',
+ url: 'https://picsum.photos/seed/shot3/600/400',
+ count: 3,
+ total_count: 3
+ },
+ status: 'success',
+ intent_type: 'procedure'
+ },
+ // 用户反馈
+ {
+ id: 12,
+ role: 'user',
+ content: JSON.stringify([{
+ type: 'text',
+ content: '这个故事设计太棒了!特别喜欢艾米丽和小星的互动场景。'
+ }]),
+ created_at: '2024-03-20T10:10:00Z',
+ message_type: undefined,
+ function_name: undefined,
+ custom_data: undefined,
+ status: 'success',
+ intent_type: 'function_call'
+ },
+ // 助手回复
+ {
+ id: 13,
+ role: 'assistant',
+ content: JSON.stringify([{
+ type: 'text',
+ content: '谢谢您的肯定!我们可以继续优化任何场景或角色设计,您觉得有什么地方需要调整吗?'
+ }]),
+ created_at: '2024-03-20T10:10:10Z',
+ message_type: undefined,
+ function_name: undefined,
+ custom_data: undefined,
+ status: 'success',
+ intent_type: 'function_call'
+ }
+];
+
+/**
+ * 类型守卫函数
+ */
+function isProjectInit(data: any): data is ProjectInit {
+ return data && 'project_data' in data;
+}
+
+function isScriptSummary(data: any): data is ScriptSummary {
+ return data && 'summary' in data;
+}
+
+function isCharacterGeneration(data: any): data is CharacterGeneration {
+ return data && 'character_name' in data && 'image_path' in data && 'count' in data && 'total_count' in data;
+}
+
+function isSketchGeneration(data: any): data is SketchGeneration {
+ return data && 'sketch_name' in data && 'image_path' in data && 'count' in data && 'total_count' in data;
+}
+
+function isShotSketchGeneration(data: any): data is ShotSketchGeneration {
+ return data && 'shot_type' in data && 'atmosphere' in data && 'key_action' in data && 'url' in data && 'count' in data && 'total_count' in data;
+}
+
+/**
+ * 系统消息转换为blocks数组
+ */
+function transformSystemMessage(
+ functionName: FunctionName,
+ content: string,
+ customData: ProjectInit | ScriptSummary | CharacterGeneration | SketchGeneration | ShotSketchGeneration
+): MessageBlock[] {
+ let blocks: MessageBlock[] = [];
+
+ switch (functionName) {
+ case 'create_project':
+ if (isProjectInit(customData)) {
+ blocks = [{
+ type: 'text',
+ text: `🎬 根据您输入的 "${customData.project_data.script}",我已完成项目的初始化。\n`
+ }, {
+ type: 'text',
+ text: content
+ }];
+ }
+ break;
+
+ case 'generate_script_summary':
+ if (isScriptSummary(customData)) {
+ blocks = [{ type: 'text', text: content }];
+ }
+ break;
+
+ case 'generate_character':
+ if (isCharacterGeneration(customData)) {
+ blocks = [{
+ type: 'text',
+ text: `🎭 演员 "${customData.character_name}" 已就位`
+ }, {
+ type: 'image',
+ url: customData.image_path
+ }, {
+ type: 'text',
+ text: '图片中演员形象仅供参考,后续可根据视频生成后进行调整。'
+ }, {
+ type: 'progress',
+ value: customData.count,
+ total: customData.total_count,
+ label: `已生成 ${customData.count} 个演员,剧本中共有 ${customData.total_count} 个`
+ }];
+ }
+ break;
+
+ case 'generate_sketch':
+ if (isSketchGeneration(customData)) {
+ blocks = [{
+ type: 'text',
+ text: `🎨 场景 "${customData.sketch_name}" 参考图片已生成 \n`
+ }, {
+ type: 'image',
+ url: customData.image_path
+ }, {
+ type: 'text',
+ text: '图片中场景仅供参考,后续可根据视频生成后进行调整。'
+ }, {
+ type: 'progress',
+ value: customData.count,
+ total: customData.total_count,
+ label: `已生成 ${customData.count} 个场景,剧本中共有 ${customData.total_count} 个`
+ }];
+ }
+ break;
+
+ case 'generate_shot_sketch':
+ if (isShotSketchGeneration(customData)) {
+ blocks = [{
+ type: 'text',
+ text: `🎬 故事板静帧生成 \n镜头类型:${customData.shot_type}\n氛围:${customData.atmosphere}\n关键动作:${customData.key_action}`
+ }, {
+ type: 'image',
+ url: customData.url
+ }, {
+ type: 'text',
+ text: '图片中故事板静帧仅供参考,后续可根据视频生成后进行调整。'
+ }, {
+ type: 'progress',
+ value: customData.count,
+ total: customData.total_count,
+ label: `已生成 ${customData.count} 个故事板静帧,剧本中共有 ${customData.total_count} 个`
+ }];
+ }
+ break;
+ }
+
+ return blocks;
+}
+
+/**
+ * 将API响应转换为ChatMessage格式
+ */
+function transformMessage(apiMessage: RealApiMessage): ChatMessage {
+ try {
+ const { id, role, content, created_at, function_name, custom_data, status, intent_type } = apiMessage;
+ let message: ChatMessage = {
+ id: id ? id.toString() : Date.now().toString(),
+ role: role,
+ createdAt: new Date(created_at).getTime(),
+ blocks: [],
+ chatType: intent_type,
+ status: status || 'success',
+ };
+
+ if (role === 'assistant' || role === 'user') {
+ const contentObj = JSON.parse(content);
+ const contentArray = Array.isArray(contentObj) ? contentObj : [contentObj];
+ contentArray.forEach((c: ApiMessageContent) => {
+ if (c.type === "text") {
+ message.blocks.push({ type: "text", text: c.content });
+ } else if (c.type === "image") {
+ message.blocks.push({ type: "image", url: c.content });
+ } else if (c.type === "video") {
+ message.blocks.push({ type: "video", url: c.content });
+ } else if (c.type === "audio") {
+ message.blocks.push({ type: "audio", url: c.content });
+ }
+ });
+ } else if (role === 'system' && function_name && custom_data) {
+ // 处理系统消息
+ message.blocks = transformSystemMessage(function_name, content, custom_data);
+ }
+
+ // 如果没有有效的 blocks,至少添加一个文本块
+ if (message.blocks.length === 0) {
+ message.blocks.push({ type: "text", text: "无内容" });
+ }
+
+ return message;
+ } catch (error) {
+ console.error("转换消息格式失败:", error, apiMessage);
+ // 返回一个带有错误信息的消息
+ return {
+ id: new Date().getTime().toString(),
+ role: apiMessage.role,
+ createdAt: new Date(apiMessage.created_at).getTime(),
+ blocks: [{ type: "text", text: "消息格式错误" }],
+ chatType: 'chat',
+ status: 'error',
+ };
+ }
+}
+
+/**
+ * 获取消息列表
+ */
+export async function fetchMessages(
+ config: ChatConfig,
+ offset: number = 0,
+ limit: number = 50
+): Promise<{
+ messages: ChatMessage[],
+ hasMore: boolean,
+ totalCount: number,
+}> {
+ const request: FetchMessagesRequest = {
+ session_id: `project_${config.projectId}_user_${config.userId}`,
+ limit,
+ offset,
+ };
+
+ try {
+ console.log('发送历史消息请求:', request);
+ const response = await post>("/intelligent/history", request);
+ console.log('收到历史消息响应:', response);
+
+ // 确保 response.data 和 messages 存在
+ if (!response.data || !response.data.messages) {
+ console.error('历史消息响应格式错误:', response);
+ return {
+ messages: [],
+ hasMore: false,
+ totalCount: 0
+ };
+ }
+
+ // 转换消息并按时间排序
+ if (response.data.messages.length === 0) {
+ return {
+ messages: MOCK_MESSAGES.map(transformMessage),
+ hasMore: false,
+ totalCount: 0
+ };
+ }
+ return {
+ messages: response.data.messages
+ .map(transformMessage)
+ .sort((a, b) => Number(a.id) - Number(b.id)),
+ hasMore: response.data.has_more,
+ totalCount: response.data.total_count
+ };
+ } catch (error) {
+ console.error("获取消息历史失败:", error);
+ throw error;
+ }
+}
+
+/**
+ * 发送新消息
+ */
+export async function sendMessage(
+ blocks: MessageBlock[],
+ config: ChatConfig
+): Promise {
+ // 提取文本和图片
+ const textBlocks = blocks.filter(b => b.type === "text");
+ const imageBlocks = blocks.filter(b => b.type === "image");
+
+ const request: SendMessageRequest = {
+ session_id: `project_${config.projectId}_user_${config.userId}`,
+ user_input: textBlocks.map(b => (b as { text: string }).text).join("\n"),
+ project_id: config.projectId,
+ user_id: config.userId,
+ };
+
+ // 如果有图片,添加第一张图片的URL
+ if (imageBlocks.length > 0) {
+ request.image_url = (imageBlocks[0] as { url: string }).url;
+ }
+
+ try {
+ console.log('发送消息请求:', request);
+ await post>("/intelligent/chat", request);
+ } catch (error) {
+ console.error("发送消息失败:", error);
+ throw error;
+ }
+}
+
+/**
+ * 重试发送消息
+ */
+export async function retryMessage(
+ messageId: string,
+ config: ChatConfig
+): Promise {
+ // TODO: 实现实际的重试逻辑,可能需要保存原始消息内容
+ // 这里简单重用发送消息的接口
+ return sendMessage([{ type: "text", text: "重试消息" }], config);
+}
\ No newline at end of file
diff --git a/components/SmartChatBox/types.ts b/components/SmartChatBox/types.ts
new file mode 100644
index 0000000..12b5547
--- /dev/null
+++ b/components/SmartChatBox/types.ts
@@ -0,0 +1,132 @@
+export type Role = "user" | "assistant" | "system";
+export type MessageStatus = 'pending' | 'success' | 'error';
+export type IntentType = 'chat' | 'function_call' | 'procedure';
+
+export type MessageBlock =
+ | { type: "text"; text: string }
+ | { type: "image"; url: string; alt?: string }
+ | { type: "video"; url: string; poster?: string }
+ | { type: "audio"; url: string }
+ | { type: "progress"; value: number; total?: number; label?: string };
+
+export interface ChatMessage {
+ id: string;
+ role: Role;
+ blocks: MessageBlock[];
+ createdAt: number;
+ chatType: IntentType;
+ status: MessageStatus;
+}
+
+export interface MessagesState {
+ messages: ChatMessage[];
+ isLoading: boolean;
+ error: Error | null;
+ hasMore: boolean;
+ loadMoreMessages: () => Promise;
+ backToLatest: () => void;
+ isViewingHistory: boolean;
+}
+
+export interface MessagesActions {
+ sendMessage: (blocks: MessageBlock[]) => Promise;
+ clearMessages: () => void;
+ retryMessage: (messageId: string) => Promise;
+}
+
+export interface SystemPushState {
+ enabled: boolean;
+ toggle: () => void;
+}
+
+export interface ChatConfig {
+ projectId: string;
+ userId: number;
+}
+
+export interface FetchMessagesRequest {
+ session_id: string;
+ limit: number;
+ offset: number;
+}
+
+export interface SendMessageRequest {
+ session_id: string;
+ user_input: string;
+ image_url?: string;
+ project_id: string;
+ user_id: number;
+}
+
+export interface ApiResponse {
+ code: number;
+ message: string;
+ data: T;
+}
+
+export interface MessagesResponse {
+ session_id: string;
+ total_count: number;
+ messages: RealApiMessage[];
+ has_more: boolean;
+ current_page: {
+ offset: number;
+ limit: number;
+ count: number;
+ };
+}
+
+type ContentType = "text" | "image" | "video" | "audio";
+export type MessageType = "project_init" | "script_summary" | "character_generation" | "sketch_generation" | "shot_sketch_generation";
+export type FunctionName = "create_project" | "generate_script_summary" | "generate_character" | "generate_sketch" | "generate_shot_sketch";
+
+// 项目创建
+export interface ProjectInit {
+ project_data: {
+ script: string; // 原始剧本
+ }
+}
+// 剧本总结
+export interface ScriptSummary {
+ summary: string; // 剧本总结
+}
+// 角色生成
+export interface CharacterGeneration {
+ character_name: string; // 角色名称
+ image_path: string; // 角色图片
+ count: number; // 生成数量
+ total_count: number; // 总数量
+}
+// 场景草图生成
+export interface SketchGeneration {
+ sketch_name: string; // 场景草图名称
+ image_path: string; // 场景草图图片
+ count: number; // 生成数量
+ total_count: number; // 总数量
+}
+// 分镜草图生成
+export interface ShotSketchGeneration {
+ shot_type: string; // 分镜镜头类型
+ atmosphere: string; // 氛围描述
+ key_action: string; // 关键动作描述
+ url: string; // 分镜草图图片
+ count: number; // 生成数量
+ total_count: number; // 总数量
+}
+
+export interface ApiMessageContent {
+ type: ContentType;
+ content: string;
+}
+
+export interface RealApiMessage {
+ created_at: string;
+ id: number;
+ role: Role;
+ content: string;
+ message_type?: MessageType;
+ function_name?: FunctionName;
+ custom_data?: ProjectInit | ScriptSummary | CharacterGeneration | SketchGeneration | ShotSketchGeneration;
+ status: MessageStatus;
+ intent_type: IntentType;
+}
\ No newline at end of file
diff --git a/components/SmartChatBox/useMessages.ts b/components/SmartChatBox/useMessages.ts
new file mode 100644
index 0000000..e0068f3
--- /dev/null
+++ b/components/SmartChatBox/useMessages.ts
@@ -0,0 +1,212 @@
+import { useCallback, useEffect, useRef, useState, useMemo } from "react";
+import { ChatMessage, MessageBlock, MessagesState, MessagesActions, SystemPushState, ChatConfig } from "./types";
+import { fetchMessages, sendMessage, retryMessage } from "./api";
+import { uid } from "./utils";
+
+const POLLING_INTERVAL = 10000; // 10秒轮询一次
+const PAGE_SIZE = 20;
+
+interface UseMessagesProps {
+ config: ChatConfig;
+ onMessagesUpdate?: (shouldScroll: boolean) => void;
+}
+
+export function useMessages({ config, onMessagesUpdate }: UseMessagesProps): [MessagesState, MessagesActions, SystemPushState] {
+ // 消息状态
+ const [displayMessages, setDisplayMessages] = useState([]);
+ const [latestMessages, setLatestMessages] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [hasMore, setHasMore] = useState(false);
+ const [offset, setOffset] = useState(0);
+ const [totalCount, setTotalCount] = useState(0);
+
+ // 系统推送状态
+ const [systemPush, setSystemPush] = useState(true);
+
+ // 状态引用
+ const configRef = useRef(config);
+ const isInitialLoadRef = useRef(true);
+ const timeoutIdRef = useRef(null);
+ const isViewingHistoryRef = useRef(false);
+ const prevTotalCountRef = useRef(totalCount);
+
+ // 更新 config 引用
+ useEffect(() => {
+ configRef.current = config;
+ }, [config]);
+
+ // 监听总消息数变化
+ useEffect(() => {
+ if (!isViewingHistoryRef.current && totalCount !== prevTotalCountRef.current) {
+ // 有新消息,需要滚动
+ onMessagesUpdate?.(true);
+ }
+ prevTotalCountRef.current = totalCount;
+ }, [totalCount, onMessagesUpdate]);
+
+ // 获取最新消息
+ const fetchLatestMessages = useCallback(async (showLoading: boolean = false) => {
+ try {
+ if (showLoading) {
+ setIsLoading(true);
+ }
+
+ const response = await fetchMessages(configRef.current, 0, PAGE_SIZE);
+ setLatestMessages(response.messages);
+ if (!isViewingHistoryRef.current) {
+ setDisplayMessages(response.messages);
+ }
+ setTotalCount(response.totalCount);
+ setHasMore(response.hasMore);
+ setOffset(PAGE_SIZE);
+ } catch (err) {
+ console.error("获取消息失败:", err);
+ setError(err instanceof Error ? err : new Error("获取消息失败"));
+ } finally {
+ if (showLoading) {
+ setIsLoading(false);
+ }
+ }
+ }, []);
+
+ // 加载更多历史消息
+ const loadMoreMessages = useCallback(async () => {
+ if (isLoading || !hasMore) return;
+
+ try {
+ isViewingHistoryRef.current = true;
+ setIsLoading(true);
+
+ const response = await fetchMessages(configRef.current, offset, PAGE_SIZE);
+ setDisplayMessages(prev => [...response.messages, ...prev]);
+ setHasMore(response.hasMore);
+ setOffset(prev => prev + PAGE_SIZE);
+ setTotalCount(response.totalCount);
+ } catch (err) {
+ console.error("加载历史消息失败:", err);
+ setError(err instanceof Error ? err : new Error("加载历史消息失败"));
+ } finally {
+ setIsLoading(false);
+ }
+ }, [offset, hasMore, isLoading]);
+
+ // 返回最新消息
+ const backToLatest = useCallback(async () => {
+ isViewingHistoryRef.current = false;
+ setDisplayMessages(latestMessages);
+ onMessagesUpdate?.(true);
+ }, [latestMessages, onMessagesUpdate]);
+
+ // 发送消息
+ const handleSendMessage = useCallback(async (blocks: MessageBlock[]) => {
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ // 立即添加用户消息(临时显示)
+ const userMessage: ChatMessage = {
+ id: uid(),
+ role: "user",
+ createdAt: Date.now(),
+ blocks,
+ chatType: 'chat',
+ status: 'pending',
+ };
+
+ // 无论是否在查看历史,发送消息后都切换到最新视图
+ isViewingHistoryRef.current = false;
+ setDisplayMessages(prev => [...prev, userMessage]);
+
+ // 发送到服务器
+ await sendMessage(blocks, configRef.current);
+
+ // 立即获取最新的消息列表
+ await fetchLatestMessages(false);
+ } catch (err) {
+ console.error("发送消息失败:", err);
+ setError(err instanceof Error ? err : new Error("发送消息失败"));
+ } finally {
+ setIsLoading(false);
+ }
+ }, [fetchLatestMessages]);
+
+ // 重试消息
+ const handleRetryMessage = useCallback(async (messageId: string) => {
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ // 重试时切换到最新视图
+ isViewingHistoryRef.current = false;
+ await retryMessage(messageId, configRef.current);
+ await fetchLatestMessages(false);
+ } catch (err) {
+ console.error("重试消息失败:", err);
+ setError(err instanceof Error ? err : new Error("重试消息失败"));
+ } finally {
+ setIsLoading(false);
+ }
+ }, [fetchLatestMessages]);
+
+ // 清空消息
+ const handleClearMessages = useCallback(() => {
+ setDisplayMessages([]);
+ setLatestMessages([]);
+ setHasMore(false);
+ setOffset(0);
+ setTotalCount(0);
+ }, []);
+
+ // 系统推送开关
+ const toggleSystemPush = useCallback(() => {
+ setSystemPush(prev => !prev);
+ }, []);
+
+ // 轮询获取最新消息
+ useEffect(() => {
+ if (!systemPush) return;
+
+ const poll = async () => {
+ await fetchLatestMessages(false);
+ timeoutIdRef.current = setTimeout(poll, POLLING_INTERVAL);
+ };
+
+ poll();
+
+ return () => {
+ if (timeoutIdRef.current) {
+ clearTimeout(timeoutIdRef.current);
+ }
+ };
+ }, [systemPush, fetchLatestMessages]);
+
+ // 初始加载
+ useEffect(() => {
+ if (isInitialLoadRef.current) {
+ isInitialLoadRef.current = false;
+ fetchLatestMessages(true);
+ }
+ }, [fetchLatestMessages]);
+
+ return [
+ {
+ messages: displayMessages,
+ isLoading,
+ error,
+ hasMore,
+ loadMoreMessages,
+ backToLatest,
+ isViewingHistory: isViewingHistoryRef.current
+ },
+ {
+ sendMessage: handleSendMessage,
+ clearMessages: handleClearMessages,
+ retryMessage: handleRetryMessage
+ },
+ {
+ enabled: systemPush,
+ toggle: toggleSystemPush
+ }
+ ];
+}
\ No newline at end of file
diff --git a/components/SmartChatBox/utils.ts b/components/SmartChatBox/utils.ts
new file mode 100644
index 0000000..fa55e18
--- /dev/null
+++ b/components/SmartChatBox/utils.ts
@@ -0,0 +1,17 @@
+import { motion } from "framer-motion";
+
+export const uid = () => Math.random().toString(36).slice(2);
+
+export const bubbleVariants = {
+ hidden: (isUser: boolean) => ({ opacity: 0, x: isUser ? 40 : -40, scale: 0.98 }),
+ visible: { opacity: 1, x: 0, scale: 1 },
+};
+
+// Format a time like 10:24
+export function hhmm(ts: number) {
+ const d = new Date(ts);
+ return `${d.getHours().toString().padStart(2, "0")}:${d
+ .getMinutes()
+ .toString()
+ .padStart(2, "0")}`;
+}
\ No newline at end of file
diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx
index 5b8c011..cbc6d3f 100644
--- a/components/pages/work-flow.tsx
+++ b/components/pages/work-flow.tsx
@@ -2,7 +2,6 @@
import React, { useRef, useEffect, useCallback } from "react";
import "./style/work-flow.css";
import { Skeleton } from "@/components/ui/skeleton";
-import { AISuggestionBar } from "@/components/ai-suggestion-bar";
import { EditModal } from "@/components/ui/edit-modal";
import { ErrorBoundary } from "@/components/ui/error-boundary";
import { TaskInfo } from "./work-flow/task-info";
@@ -10,11 +9,13 @@ 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 } from "lucide-react";
+import { AlertCircle, RefreshCw, Pause, Play, ChevronLast, MessageSquareText } 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';
const WorkFlow = React.memo(function WorkFlow() {
useEffect(() => {
@@ -24,10 +25,13 @@ const WorkFlow = React.memo(function WorkFlow() {
const containerRef = useRef(null);
const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
const [activeEditTab, setActiveEditTab] = React.useState('1');
+ const [isSmartChatBoxOpen, setIsSmartChatBoxOpen] = React.useState(true);
const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || '';
+ const userId = JSON.parse(localStorage.getItem("currentUser") || '{}').id || NaN;
+
SaveEditUseCase.setProjectId(episodeId);
// 使用自定义 hooks 管理状态
const {
@@ -71,14 +75,6 @@ const WorkFlow = React.memo(function WorkFlow() {
setIsEditModalOpen(true);
}, []);
- const handleSuggestionClick = useCallback((suggestion: string) => {
- console.log('Selected suggestion:', suggestion);
- }, []);
-
- const handleSubmit = useCallback((text: string) => {
- console.log('Submitted text:', text);
- }, []);
-
return (
@@ -157,7 +153,7 @@ const WorkFlow = React.memo(function WorkFlow() {
{taskObject.currentStage !== 'final_video' && taskObject.currentStage !== 'script' && (
setIsPauseWorkFlow(!isPauseWorkFlow)}
/>
{ !mode.includes('auto') && (
)}
@@ -190,6 +186,50 @@ const WorkFlow = React.memo(function WorkFlow() {
)
}
+ {/* 智能对话按钮 */}
+
+ setIsSmartChatBoxOpen(true)}
+ />
+
+
+ {/* 智能对话弹窗 */}
+ setIsSmartChatBoxOpen(false)}
+ >
+
+
+
window.removeEventListener('keydown', handleKeyDown);
- }, [handleKeyDown]);
+ }, [handleKeyDown, isDisabledFocus]);
// 确保在数据变化时保持焦点
useEffect(() => {
diff --git a/next.config.js b/next.config.js
index 45a8971..4fe354f 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,4 +1,5 @@
const BASE_URL = 'https://77.smartvideo.py.qikongjian.com'
+// const BASE_URL = 'http://192.168.120.36:8000'
/** @type {import('next').NextConfig} */
const nextConfig = {
eslint: {