diff --git a/.gitignore b/.gitignore
index 6c24455..160f936 100644
Binary files a/.gitignore and b/.gitignore differ
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/core/models/chat_history.py b/backend/core/models/chat_history.py
index 5f96ae9..793e1a5 100644
--- a/backend/core/models/chat_history.py
+++ b/backend/core/models/chat_history.py
@@ -15,6 +15,14 @@ class Message(BaseModel):
description="消息发送时间戳"
)
floor: int = Field(0, description="对话楼层数")
+ swipes: List[str] = Field(
+ default_factory=list,
+ description="历史版本列表。用户消息:存编辑过的不同版本。AI消息:存重roll生成的不同版本"
+ )
+ swipe_id: int = Field(
+ 0,
+ description="当前指针。指示当前显示的是 swipes 数组中的第几个(从 0 开始)"
+ )
mes: str = Field(..., description="消息内容文本")
extra: Dict[str, Any] = Field(
default_factory=dict,
@@ -211,17 +219,26 @@ class ChatHistory(BaseModel):
"name": str,
"is_user": bool,
"floor": int,
- "mes": str
+ "mes": str,
+ "swipes": List[str],
+ "swipe_id": int
}
"""
# 创建消息字典列表
messages_list = []
for msg in self.messages:
+ # 获取当前消息内容:优先从swipes数组中获取,如果不存在则使用mes
+ current_mes = msg.mes
+ if msg.swipes and 0 <= msg.swipe_id < len(msg.swipes):
+ current_mes = msg.swipes[msg.swipe_id]
+
msg_dict = {
"name": msg.name,
"is_user": msg.is_user,
"floor": msg.floor,
- "mes": msg.mes
+ "mes": current_mes,
+ "swipes": msg.swipes,
+ "swipe_id": msg.swipe_id
}
messages_list.append(msg_dict)
diff --git a/frontend-react/Dockerfile b/frontend-react/Dockerfile
index 2e2ffb2..2402881 100644
--- a/frontend-react/Dockerfile
+++ b/frontend-react/Dockerfile
@@ -1,6 +1,6 @@
# 使用 Node.js 18 Alpine 镜像作为基础
# 必须明确指定 node 版本,否则可能默认为空或 python 镜像
-FROM node:18-alpine
+FROM node:20-alpine
# 设置工作目录
WORKDIR /app
diff --git a/frontend-react/src/Store/Slices/ChatBoxSlice.jsx b/frontend-react/src/Store/Slices/ChatBoxSlice.jsx
index 578acab..ca1337a 100644
--- a/frontend-react/src/Store/Slices/ChatBoxSlice.jsx
+++ b/frontend-react/src/Store/Slices/ChatBoxSlice.jsx
@@ -3,88 +3,147 @@ import { subscribeWithSelector } from 'zustand/middleware';
const useChatBoxStore = create(
subscribeWithSelector((set, get) => ({
- // 聊天历史消息列表
- messages: [],
+ // 聊天历史消息列表
+ messages: [],
- // 用户名称
- userName: '',
+ // 用户名称
+ userName: '',
- // 角色名称
- characterName: '',
+ // 角色名称
+ characterName: '',
- // 当前选中的角色
- currentRole: null,
+ // 当前选中的角色
+ currentRole: null,
- // 当前选中的聊天
- currentChat: null,
+ // 当前选中的聊天
+ currentChat: null,
- // 是否正在加载
- isLoading: false,
+ // 是否正在加载
+ isLoading: false,
- // 错误信息
- error: null,
+ // 是否正在生成
+ isGenerating: false,
- // 设置消息列表
- setMessages: (messages) => set({ messages }),
+ // 错误信息
+ error: null,
- // 设置用户名称
- setUserName: (userName) => set({ userName }),
+ // 设置消息列表
+ setMessages: (messages) => set({ messages }),
- // 设置角色名称
- setCharacterName: (characterName) => set({ characterName }),
+ // 设置用户名称
+ setUserName: (userName) => set({ userName }),
- // 设置当前角色
- setCurrentRole: (role) => set({ currentRole: role }),
+ // 设置角色名称
+ setCharacterName: (characterName) => set({ characterName }),
- // 设置当前聊天
- setCurrentChat: (chat) => set({ currentChat: chat }),
+ // 设置当前角色
+ setCurrentRole: (role) => set({ currentRole: role }),
- // 同时设置角色和聊天
- setChatBoxRoleAndChat: (role, chat) => set({
- currentRole: role,
- currentChat: chat
- }),
+ // 设置当前聊天
+ setCurrentChat: (chat) => set({ currentChat: chat }),
-
- // 加载聊天历史
- fetchChatHistory: async (roleName, chatName) => {
- set({ isLoading: true, error: null });
- try {
- const response = await fetch(`/api/chat_box/get_chat_history?role_name=${encodeURIComponent(roleName)}&chat_name=${encodeURIComponent(chatName)}`);
- if (!response.ok) {
- throw new Error('Failed to fetch chat history');
- }
- const data = await response.json();
- // 修改数据处理逻辑,适配API返回的数据结构
- set({
- messages: data || [], // 直接使用返回的数组
- userName: 'User', // 固定用户名
- characterName: roleName || 'Assistant', // 使用角色名作为角色名称
- isLoading: false
- });
- } catch (error) {
- set({
- error: error.message,
- isLoading: false
- });
- }
+ // 同时设置角色和聊天
+ setChatBoxRoleAndChat: (role, chat) => {
+ console.log('[ChatBoxStore] setChatBoxRoleAndChat called with:', { role, chat });
+ set({
+ currentRole: role,
+ currentChat: chat
+ });
},
- // 清空聊天历史
- clearChatHistory: () => set({
- messages: [],
- userName: '',
- characterName: '',
- error: null
- }),
+ // 设置生成状态
+ setIsGenerating: (status) => set({ isGenerating: status }),
- // 更新特定消息的内容
- updateMessage: (id, content) => set((state) => ({
- messages: state.messages.map((msg) =>
- msg.id === id ? { ...msg, content } : msg
- )
- })),
+ sendMessage: async (content) => {
+ const { messages, userName, characterName, currentRole, currentChat } = get();
+
+ set({
+ isGenerating: true,
+ messages: [...messages, {
+ id: Date.now(),
+ floor: messages.length + 1,
+ mes: content,
+ is_user: true
+ }]
+ });
+
+ try {
+ const response = await fetch('/api/chat_box/send_message', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ role_name: currentRole,
+ chat_name: currentChat,
+ message: content
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to send message');
+ }
+
+ const data = await response.json();
+ set((state) => ({
+ messages: [...state.messages, {
+ id: Date.now(),
+ floor: state.messages.length + 1,
+ mes: data.response,
+ is_user: false
+ }],
+ isGenerating: false
+ }));
+ } catch (error) {
+ set({
+ error: error.message,
+ isGenerating: false
+ });
+ }
+ },
+
+ // 终止生成
+ stopGeneration: () => set({ isGenerating: false }),
+ // 加载聊天历史
+ fetchChatHistory: async (roleName, chatName) => {
+ set({ isLoading: true, error: null });
+ try {
+ const response = await fetch(`/api/chat_box/get_chat_history?role_name=${encodeURIComponent(roleName)}&chat_name=${encodeURIComponent(chatName)}`);
+ if (!response.ok) {
+ throw new Error('Failed to fetch chat history');
+ }
+ const data = await response.json();
+ // 修改数据处理逻辑,适配API返回的数据结构
+ set({
+ messages: data || [], // 直接使用返回的数组
+ userName: 'User', // 固定用户名
+ characterName: roleName || 'Assistant', // 使用角色名作为角色名称
+ isLoading: false
+ });
+ } catch (error) {
+ set({
+ error: error.message,
+ isLoading: false
+ });
+ }
+ },
+
+
+ // 清空聊天历史
+ clearChatHistory: () => set({
+ messages: [],
+ userName: '',
+ characterName: '',
+ error: null
+ }),
+
+ // 更新特定消息的内容
+ updateMessage: (id, content) => set((state) => ({
+ messages: state.messages.map((msg) =>
+ msg.id === id ? { ...msg, content } : msg
+ )
+ })),
}))
);
@@ -92,15 +151,15 @@ const useChatBoxStore = create(
useChatBoxStore.subscribe(
(state) => ({ role: state.currentRole, chat: state.currentChat }),
({ role, chat }, prev) => {
- // 只有当角色或聊天发生变化时才处理
- if (role !== prev.role || chat !== prev.chat) {
- // 确保角色和聊天都存在且不为null
- if (role && chat) {
- useChatBoxStore.getState().fetchChatHistory(role, chat);
- } else {
- useChatBoxStore.getState().clearChatHistory();
- }
- }
+ // 只有当角色或聊天发生变化时才处理
+ if (role !== prev.role || chat !== prev.chat) {
+ // 确保角色和聊天都存在且不为null
+ if (role && chat) {
+ useChatBoxStore.getState().fetchChatHistory(role, chat);
+ } else {
+ useChatBoxStore.getState().clearChatHistory();
+ }
+ }
},
{ equalityFn: (a, b) => a.role === b.role && a.chat === b.chat }
);
diff --git a/frontend-react/src/components/ChatBox/ChatBox.jsx b/frontend-react/src/components/ChatBox/ChatBox.jsx
index a75240a..4834951 100644
--- a/frontend-react/src/components/ChatBox/ChatBox.jsx
+++ b/frontend-react/src/components/ChatBox/ChatBox.jsx
@@ -6,128 +6,181 @@ const ChatBox = () => {
const [editingId, setEditingId] = useState(null);
const [editContent, setEditContent] = useState('');
const messagesEndRef = useRef(null);
+ const [inputValue, setInputValue] = useState('');
- // 从 ChatBoxStore 获取状态和方法
- const {
- messages,
- isLoading,
- error,
- userName,
- characterName,
- updateMessage
- } = useChatBoxStore();
+ // 从 ChatBoxStore 获取状态和方法
+ const {
+ messages,
+ isLoading,
+ error,
+ userName,
+ characterName,
+ updateMessage,
+ isGenerating,
+ sendMessage,
+ stopGeneration
+ } = useChatBoxStore();
+
+ const [inputHeight, setInputHeight] = useState(42);
+
+ // 添加输入框高度自适应处理
+ const handleInputHeight = (e) => {
+ const textarea = e.target;
+ const newHeight = Math.min(Math.max(textarea.scrollHeight, 42), 300);
+ setInputHeight(newHeight);
+ };
+
+ // 处理发送或终止
+ const handleSendOrStop = () => {
+ if (isGenerating) {
+ stopGeneration();
+ } else {
+ sendMessage(inputValue);
+ setInputValue('');
+ setInputHeight(42);
+ }
+ };
+
+ // 处理键盘事件
+ const handleKeyDown = (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSendOrStop();
+ }
+ };
// 自动滚动到底部
const scrollToBottom = () => {
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
- scrollToBottom();
+ scrollToBottom();
}, [messages]);
// 处理编辑消息
const handleEdit = (message) => {
- setEditingId(message.floor);
- setEditContent(message.mes);
+ setEditingId(message.floor);
+ setEditContent(message.mes);
};
// 保存编辑
const handleSaveEdit = (messageId) => {
- // 调用 store 中的 updateMessage 方法
- updateMessage(messageId, editContent);
- setEditingId(null);
- setEditContent('');
+ // 调用 store 中的 updateMessage 方法
+ updateMessage(messageId, editContent);
+ setEditingId(null);
+ setEditContent('');
};
// 取消编辑
const handleCancelEdit = () => {
- setEditingId(null);
- setEditContent('');
+ setEditingId(null);
+ setEditContent('');
};
// 渲染单条消息
const renderMessage = (message) => {
- const isUser = message.is_user;
- const isEditing = editingId === message.floor;
+ const isUser = message.is_user;
+ const isEditing = editingId === message.floor;
- // 根据消息类型设置显示名称
- const displayName = isUser ? userName : characterName;
+ // 根据消息类型设置显示名称
+ const displayName = isUser ? userName : characterName;
- return (
-
-
-
-
{displayName}
-
#{message.floor}
-
-
- handleEdit(message)}
- title="编辑"
- >
- ✎
-
-
- •••
-
-
-
-
-
- {isEditing ? (
-
- ) : (
-
- {message.mes}
-
- )}
+ return (
+
+
+
+
{displayName}
+
#{message.floor}
+
+
+ handleEdit(message)}
+ title="编辑"
+ >
+ ✎
+
+
+ •••
+
+
+
+
+
+ {isEditing ? (
+
+ ) : (
+
+ {message.mes}
+
+ )}
+
+
+
+ );
+ };
+
+return (
+
+
+ {isLoading ? (
+
加载中...
+ ) : error ? (
+
{error}
+ ) : messages.length === 0 ? (
+
暂无消息
+ ) : (
+ messages.map(renderMessage)
+ )}
+
+
+
+
+
+
+ {isGenerating ? '终止' : '发送'}
+
);
- };
-
- return (
-
-
- {isLoading ? (
-
加载中...
- ) : error ? (
-
{error}
- ) : messages.length === 0 ? (
-
暂无消息
- ) : (
- messages.map(renderMessage)
- )}
-
-
-
- );
};
export default ChatBox;
diff --git a/frontend-react/src/components/Markdown2Html/Markdown2Html.jsx b/frontend-react/src/components/Markdown2Html/Markdown2Html.jsx
index 8406a21..a2e2e69 100644
--- a/frontend-react/src/components/Markdown2Html/Markdown2Html.jsx
+++ b/frontend-react/src/components/Markdown2Html/Markdown2Html.jsx
@@ -1,146 +1,146 @@
-// npm install react react-dom react-markdown react-syntax-highlighter remark-math rehype-katex react-copy-to-clipboard mermaid katex
-import React, { useState, useCallback, useEffect } from "react";
-import ReactMarkdown from "react-markdown";
-import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
-import { materialLight } from "react-syntax-highlighter/dist/cjs/styles/prism";
-import remarkMath from "remark-math";
-import rehypeKatex from "rehype-katex";
-import { CopyToClipboard } from "react-copy-to-clipboard";
-import mermaid from "mermaid";
-import "katex/dist/katex.min.css";
-
-// Mermaid 初始化配置
-// Mermaid是一个画图的语言
-mermaid.initialize({
- startOnLoad: false,
- theme: "default",
- securityLevel: "loose",
-});
-
-const DownSvg = () => (
-
-
-
-);
-
-const CopySvg = () => (
-
-
-
-);
-
-
-const MermaidChart= ({ code }) => {
- const [svg, setSvg] = useState("");
- const [id] = useState(() => `mermaid-${Math.random().toString(36).substr(2, 9)}`);
-
- useEffect(() => {
- try {
- mermaid.parse(code);
- mermaid.render(id, code).then(({ svg }) => setSvg(svg));
- } catch (err) {
- setSvg(`
Error rendering mermaid: ${err} `);
- }
- }, [code, id]);
-
- return
;
-};
-
-const MarkdownToHTML= ({ markdownRAW, className }) => {
- const [isMermaidLoaded] = useState(true);
- // 渲染代码块的方法
- const CodeBlock = useCallback(
- ({ node, inline, className, children, ...props } ) => {
- const [isShowCode, setIsShowCode] = useState(true); // 添加展开与收起代码块状态
- const [isShowCopy, setIsShowCopy] = useState(false);// 添加点击复制的状态
- const match = /language-(\w+)/.exec(className || ""); // 这是用来匹配代码块对应语言的方法
- const codeContent = String(children).replace(/\n$/, "");
- // 处理复制成功提示
- const handleCopy = () => {
- setIsShowCopy(true);
- setTimeout(() => setIsShowCopy(false), 1500);
- };
- //处理单行代码
- if (inline) {
- return
{children};
- }
-
- // 处理 Mermaid 图表代码
- if (match?.[1] === "mermaid" && isMermaidLoaded) {
- return (
-
-
-
setIsShowCode(!isShowCode)}
- >
-
-
-
{match[1]}
-
-
- {isShowCopy && ✓ 复制成功 }
-
-
-
-
- {isShowCode &&
}
-
- );
- }
-
- return (
-
-
-
setIsShowCode(!isShowCode)}
- >
-
-
-
{match?.[1] || "code"}
-
-
- {isShowCopy && ✓ 复制成功 }
-
-
-
-
- {isShowCode && (
-
- {codeContent}
-
- )}
-
- );
- },
- [isMermaidLoaded]
- );
-
- return (
-
-
- {markdownRAW}
-
-
-
- );
-};
-
-export default MarkdownToHTML;
\ No newline at end of file
+// // npm install react react-dom react-markdown react-syntax-highlighter remark-math rehype-katex react-copy-to-clipboard mermaid katex
+// import React, { useState, useCallback, useEffect } from "react";
+// import ReactMarkdown from "react-markdown";
+// import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
+// import { materialLight } from "react-syntax-highlighter/dist/cjs/styles/prism";
+// import remarkMath from "remark-math";
+// import rehypeKatex from "rehype-katex";
+// import { CopyToClipboard } from "react-copy-to-clipboard";
+// import mermaid from "mermaid";
+// import "katex/dist/katex.min.css";
+//
+// // Mermaid 初始化配置
+// // Mermaid是一个画图的语言
+// mermaid.initialize({
+// startOnLoad: false,
+// theme: "default",
+// securityLevel: "loose",
+// });
+//
+// const DownSvg = () => (
+//
+//
+//
+// );
+//
+// const CopySvg = () => (
+//
+//
+//
+// );
+//
+//
+// const MermaidChart= ({ code }) => {
+// const [svg, setSvg] = useState("");
+// const [id] = useState(() => `mermaid-${Math.random().toString(36).substr(2, 9)}`);
+//
+// useEffect(() => {
+// try {
+// mermaid.parse(code);
+// mermaid.render(id, code).then(({ svg }) => setSvg(svg));
+// } catch (err) {
+// setSvg(`
Error rendering mermaid: ${err} `);
+// }
+// }, [code, id]);
+//
+// return
;
+// };
+//
+// const MarkdownToHTML= ({ markdownRAW, className }) => {
+// const [isMermaidLoaded] = useState(true);
+// // 渲染代码块的方法
+// const CodeBlock = useCallback(
+// ({ node, inline, className, children, ...props } ) => {
+// const [isShowCode, setIsShowCode] = useState(true); // 添加展开与收起代码块状态
+// const [isShowCopy, setIsShowCopy] = useState(false);// 添加点击复制的状态
+// const match = /language-(\w+)/.exec(className || ""); // 这是用来匹配代码块对应语言的方法
+// const codeContent = String(children).replace(/\n$/, "");
+// // 处理复制成功提示
+// const handleCopy = () => {
+// setIsShowCopy(true);
+// setTimeout(() => setIsShowCopy(false), 1500);
+// };
+// //处理单行代码
+// if (inline) {
+// return
{children};
+// }
+//
+// // 处理 Mermaid 图表代码
+// if (match?.[1] === "mermaid" && isMermaidLoaded) {
+// return (
+//
+//
+//
setIsShowCode(!isShowCode)}
+// >
+//
+//
+//
{match[1]}
+//
+//
+// {isShowCopy && ✓ 复制成功 }
+//
+//
+//
+//
+// {isShowCode &&
}
+//
+// );
+// }
+//
+// return (
+//
+//
+//
setIsShowCode(!isShowCode)}
+// >
+//
+//
+//
{match?.[1] || "code"}
+//
+//
+// {isShowCopy && ✓ 复制成功 }
+//
+//
+//
+//
+// {isShowCode && (
+//
+// {codeContent}
+//
+// )}
+//
+// );
+// },
+// [isMermaidLoaded]
+// );
+//
+// return (
+//
+//
+// {markdownRAW}
+//
+//
+//
+// );
+// };
+//
+// export default MarkdownToHTML;
\ No newline at end of file
diff --git a/frontend/assets/images/ceaa019ee659f86fc8e2a22217df30f7.jpg b/frontend/assets/images/ceaa019ee659f86fc8e2a22217df30f7.jpg
index 41b964d..e69de29 100644
Binary files a/frontend/assets/images/ceaa019ee659f86fc8e2a22217df30f7.jpg and b/frontend/assets/images/ceaa019ee659f86fc8e2a22217df30f7.jpg differ