修补输入框
This commit is contained in:
BIN
.gitignore
vendored
BIN
.gitignore
vendored
Binary file not shown.
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
@@ -15,6 +15,14 @@ class Message(BaseModel):
|
|||||||
description="消息发送时间戳"
|
description="消息发送时间戳"
|
||||||
)
|
)
|
||||||
floor: int = Field(0, 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="消息内容文本")
|
mes: str = Field(..., description="消息内容文本")
|
||||||
extra: Dict[str, Any] = Field(
|
extra: Dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
@@ -211,17 +219,26 @@ class ChatHistory(BaseModel):
|
|||||||
"name": str,
|
"name": str,
|
||||||
"is_user": bool,
|
"is_user": bool,
|
||||||
"floor": int,
|
"floor": int,
|
||||||
"mes": str
|
"mes": str,
|
||||||
|
"swipes": List[str],
|
||||||
|
"swipe_id": int
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
# 创建消息字典列表
|
# 创建消息字典列表
|
||||||
messages_list = []
|
messages_list = []
|
||||||
for msg in self.messages:
|
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 = {
|
msg_dict = {
|
||||||
"name": msg.name,
|
"name": msg.name,
|
||||||
"is_user": msg.is_user,
|
"is_user": msg.is_user,
|
||||||
"floor": msg.floor,
|
"floor": msg.floor,
|
||||||
"mes": msg.mes
|
"mes": current_mes,
|
||||||
|
"swipes": msg.swipes,
|
||||||
|
"swipe_id": msg.swipe_id
|
||||||
}
|
}
|
||||||
messages_list.append(msg_dict)
|
messages_list.append(msg_dict)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 使用 Node.js 18 Alpine 镜像作为基础
|
# 使用 Node.js 18 Alpine 镜像作为基础
|
||||||
# 必须明确指定 node 版本,否则可能默认为空或 python 镜像
|
# 必须明确指定 node 版本,否则可能默认为空或 python 镜像
|
||||||
FROM node:18-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
# 设置工作目录
|
# 设置工作目录
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ const useChatBoxStore = create(
|
|||||||
// 是否正在加载
|
// 是否正在加载
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|
||||||
|
// 是否正在生成
|
||||||
|
isGenerating: false,
|
||||||
|
|
||||||
// 错误信息
|
// 错误信息
|
||||||
error: null,
|
error: null,
|
||||||
|
|
||||||
@@ -40,12 +43,68 @@ const useChatBoxStore = create(
|
|||||||
setCurrentChat: (chat) => set({ currentChat: chat }),
|
setCurrentChat: (chat) => set({ currentChat: chat }),
|
||||||
|
|
||||||
// 同时设置角色和聊天
|
// 同时设置角色和聊天
|
||||||
setChatBoxRoleAndChat: (role, chat) => set({
|
setChatBoxRoleAndChat: (role, chat) => {
|
||||||
|
console.log('[ChatBoxStore] setChatBoxRoleAndChat called with:', { role, chat });
|
||||||
|
set({
|
||||||
currentRole: role,
|
currentRole: role,
|
||||||
currentChat: chat
|
currentChat: chat
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// 设置生成状态
|
||||||
|
setIsGenerating: (status) => set({ isGenerating: status }),
|
||||||
|
|
||||||
|
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) => {
|
fetchChatHistory: async (roleName, chatName) => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const ChatBox = () => {
|
|||||||
const [editingId, setEditingId] = useState(null);
|
const [editingId, setEditingId] = useState(null);
|
||||||
const [editContent, setEditContent] = useState('');
|
const [editContent, setEditContent] = useState('');
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
// 从 ChatBoxStore 获取状态和方法
|
// 从 ChatBoxStore 获取状态和方法
|
||||||
const {
|
const {
|
||||||
@@ -14,9 +15,40 @@ const ChatBox = () => {
|
|||||||
error,
|
error,
|
||||||
userName,
|
userName,
|
||||||
characterName,
|
characterName,
|
||||||
updateMessage
|
updateMessage,
|
||||||
|
isGenerating,
|
||||||
|
sendMessage,
|
||||||
|
stopGeneration
|
||||||
} = useChatBoxStore();
|
} = 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 = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
@@ -126,6 +158,27 @@ const ChatBox = () => {
|
|||||||
)}
|
)}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="chat-input-wrapper">
|
||||||
|
<div className="chat-input-area">
|
||||||
|
<textarea
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
setInputValue(e.target.value);
|
||||||
|
handleInputHeight(e);
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
style={{ height: `${inputHeight}px` }}
|
||||||
|
placeholder="输入消息..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={`send-button ${isGenerating ? 'stopping' : ''}`}
|
||||||
|
onClick={handleSendOrStop}
|
||||||
|
disabled={!inputValue.trim() && !isGenerating}
|
||||||
|
>
|
||||||
|
{isGenerating ? '终止' : '发送'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,146 +1,146 @@
|
|||||||
// npm install react react-dom react-markdown react-syntax-highlighter remark-math rehype-katex react-copy-to-clipboard mermaid katex
|
// // 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 React, { useState, useCallback, useEffect } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
// import ReactMarkdown from "react-markdown";
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
// import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
import { materialLight } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
// import { materialLight } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||||
import remarkMath from "remark-math";
|
// import remarkMath from "remark-math";
|
||||||
import rehypeKatex from "rehype-katex";
|
// import rehypeKatex from "rehype-katex";
|
||||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
// import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||||
import mermaid from "mermaid";
|
// import mermaid from "mermaid";
|
||||||
import "katex/dist/katex.min.css";
|
// import "katex/dist/katex.min.css";
|
||||||
|
//
|
||||||
// Mermaid 初始化配置
|
// // Mermaid 初始化配置
|
||||||
// Mermaid是一个画图的语言
|
// // Mermaid是一个画图的语言
|
||||||
mermaid.initialize({
|
// mermaid.initialize({
|
||||||
startOnLoad: false,
|
// startOnLoad: false,
|
||||||
theme: "default",
|
// theme: "default",
|
||||||
securityLevel: "loose",
|
// securityLevel: "loose",
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
const DownSvg = () => (
|
// const DownSvg = () => (
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24">
|
// <svg width="12" height="12" viewBox="0 0 24 24">
|
||||||
<path d="M7 10l5 5 5-5z" fill="currentColor" />
|
// <path d="M7 10l5 5 5-5z" fill="currentColor" />
|
||||||
</svg>
|
// </svg>
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
const CopySvg = () => (
|
// const CopySvg = () => (
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24">
|
// <svg width="14" height="14" viewBox="0 0 24 24">
|
||||||
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" fill="currentColor" />
|
// <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" fill="currentColor" />
|
||||||
</svg>
|
// </svg>
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
|
//
|
||||||
const MermaidChart= ({ code }) => {
|
// const MermaidChart= ({ code }) => {
|
||||||
const [svg, setSvg] = useState("");
|
// const [svg, setSvg] = useState("");
|
||||||
const [id] = useState(() => `mermaid-${Math.random().toString(36).substr(2, 9)}`);
|
// const [id] = useState(() => `mermaid-${Math.random().toString(36).substr(2, 9)}`);
|
||||||
|
//
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
try {
|
// try {
|
||||||
mermaid.parse(code);
|
// mermaid.parse(code);
|
||||||
mermaid.render(id, code).then(({ svg }) => setSvg(svg));
|
// mermaid.render(id, code).then(({ svg }) => setSvg(svg));
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
setSvg(`<pre>Error rendering mermaid: ${err}</pre>`);
|
// setSvg(`<pre>Error rendering mermaid: ${err}</pre>`);
|
||||||
}
|
// }
|
||||||
}, [code, id]);
|
// }, [code, id]);
|
||||||
|
//
|
||||||
return <div dangerouslySetInnerHTML={{ __html: svg }} />;
|
// return <div dangerouslySetInnerHTML={{ __html: svg }} />;
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
const MarkdownToHTML= ({ markdownRAW, className }) => {
|
// const MarkdownToHTML= ({ markdownRAW, className }) => {
|
||||||
const [isMermaidLoaded] = useState(true);
|
// const [isMermaidLoaded] = useState(true);
|
||||||
// 渲染代码块的方法
|
// // 渲染代码块的方法
|
||||||
const CodeBlock = useCallback(
|
// const CodeBlock = useCallback(
|
||||||
({ node, inline, className, children, ...props } ) => {
|
// ({ node, inline, className, children, ...props } ) => {
|
||||||
const [isShowCode, setIsShowCode] = useState(true); // 添加展开与收起代码块状态
|
// const [isShowCode, setIsShowCode] = useState(true); // 添加展开与收起代码块状态
|
||||||
const [isShowCopy, setIsShowCopy] = useState(false);// 添加点击复制的状态
|
// const [isShowCopy, setIsShowCopy] = useState(false);// 添加点击复制的状态
|
||||||
const match = /language-(\w+)/.exec(className || ""); // 这是用来匹配代码块对应语言的方法
|
// const match = /language-(\w+)/.exec(className || ""); // 这是用来匹配代码块对应语言的方法
|
||||||
const codeContent = String(children).replace(/\n$/, "");
|
// const codeContent = String(children).replace(/\n$/, "");
|
||||||
// 处理复制成功提示
|
// // 处理复制成功提示
|
||||||
const handleCopy = () => {
|
// const handleCopy = () => {
|
||||||
setIsShowCopy(true);
|
// setIsShowCopy(true);
|
||||||
setTimeout(() => setIsShowCopy(false), 1500);
|
// setTimeout(() => setIsShowCopy(false), 1500);
|
||||||
};
|
// };
|
||||||
//处理单行代码
|
// //处理单行代码
|
||||||
if (inline) {
|
// if (inline) {
|
||||||
return <code className={className} {...props}>{children}</code>;
|
// return <code className={className} {...props}>{children}</code>;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 处理 Mermaid 图表代码
|
// // 处理 Mermaid 图表代码
|
||||||
if (match?.[1] === "mermaid" && isMermaidLoaded) {
|
// if (match?.[1] === "mermaid" && isMermaidLoaded) {
|
||||||
return (
|
// return (
|
||||||
<div style={{ position: "relative", margin: "20px 0" }}>
|
// <div style={{ position: "relative", margin: "20px 0" }}>
|
||||||
<div className="code-header">
|
// <div className="code-header">
|
||||||
<div
|
// <div
|
||||||
style={{ cursor: "pointer", marginRight: "10px", transformOrigin: "8px" }}
|
// style={{ cursor: "pointer", marginRight: "10px", transformOrigin: "8px" }}
|
||||||
className={isShowCode ? "code-rotate-down" : "code-rotate-right"}
|
// className={isShowCode ? "code-rotate-down" : "code-rotate-right"}
|
||||||
onClick={() => setIsShowCode(!isShowCode)}
|
// onClick={() => setIsShowCode(!isShowCode)}
|
||||||
>
|
// >
|
||||||
<DownSvg />
|
// <DownSvg />
|
||||||
</div>
|
// </div>
|
||||||
<div>{match[1]}</div>
|
// <div>{match[1]}</div>
|
||||||
<CopyToClipboard text={codeContent} onCopy={handleCopy}>
|
// <CopyToClipboard text={codeContent} onCopy={handleCopy}>
|
||||||
<div className="preview-code-copy" style={{ cursor: "pointer" }}>
|
// <div className="preview-code-copy" style={{ cursor: "pointer" }}>
|
||||||
{isShowCopy && <span className="copy-success">✓ 复制成功</span>}
|
// {isShowCopy && <span className="copy-success">✓ 复制成功</span>}
|
||||||
<CopySvg />
|
// <CopySvg />
|
||||||
</div>
|
// </div>
|
||||||
</CopyToClipboard>
|
// </CopyToClipboard>
|
||||||
</div>
|
// </div>
|
||||||
{isShowCode && <MermaidChart code={codeContent} />}
|
// {isShowCode && <MermaidChart code={codeContent} />}
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return (
|
// return (
|
||||||
<div style={{ position: "relative", padding:"0"}}>
|
// <div style={{ position: "relative", padding:"0"}}>
|
||||||
<div className="code-header">
|
// <div className="code-header">
|
||||||
<div
|
// <div
|
||||||
style={{ cursor: "pointer", marginRight: "10px", transformOrigin: "8px" }}
|
// style={{ cursor: "pointer", marginRight: "10px", transformOrigin: "8px" }}
|
||||||
className={isShowCode ? "code-rotate-down" : "code-rotate-right"}
|
// className={isShowCode ? "code-rotate-down" : "code-rotate-right"}
|
||||||
onClick={() => setIsShowCode(!isShowCode)}
|
// onClick={() => setIsShowCode(!isShowCode)}
|
||||||
>
|
// >
|
||||||
<DownSvg style={{
|
// <DownSvg style={{
|
||||||
transform: isShowCode ? "rotate(0deg)" : "rotate(-90deg)",
|
// transform: isShowCode ? "rotate(0deg)" : "rotate(-90deg)",
|
||||||
transition: "transform 0.2s"
|
// transition: "transform 0.2s"
|
||||||
}} />
|
// }} />
|
||||||
</div>
|
// </div>
|
||||||
<div>{match?.[1] || "code"}</div>
|
// <div>{match?.[1] || "code"}</div>
|
||||||
<CopyToClipboard text={codeContent} onCopy={handleCopy}>
|
// <CopyToClipboard text={codeContent} onCopy={handleCopy}>
|
||||||
<div className="preview-code-copy" style={{ cursor: "pointer" }}>
|
// <div className="preview-code-copy" style={{ cursor: "pointer" }}>
|
||||||
{isShowCopy && <span className="copy-success">✓ 复制成功</span>}
|
// {isShowCopy && <span className="copy-success">✓ 复制成功</span>}
|
||||||
<CopySvg />
|
// <CopySvg />
|
||||||
</div>
|
// </div>
|
||||||
</CopyToClipboard>
|
// </CopyToClipboard>
|
||||||
</div>
|
// </div>
|
||||||
{isShowCode && (
|
// {isShowCode && (
|
||||||
<SyntaxHighlighter
|
// <SyntaxHighlighter
|
||||||
style={materialLight}
|
// style={materialLight}
|
||||||
language={match?.[1] || "text"}
|
// language={match?.[1] || "text"}
|
||||||
PreTag="div"
|
// PreTag="div"
|
||||||
showLineNumbers
|
// showLineNumbers
|
||||||
{...props}
|
// {...props}
|
||||||
>
|
// >
|
||||||
{codeContent}
|
// {codeContent}
|
||||||
</SyntaxHighlighter>
|
// </SyntaxHighlighter>
|
||||||
)}
|
// )}
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
[isMermaidLoaded]
|
// [isMermaidLoaded]
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
return (
|
// return (
|
||||||
<div
|
// <div
|
||||||
className={className} >
|
// className={className} >
|
||||||
<ReactMarkdown
|
// <ReactMarkdown
|
||||||
remarkPlugins={[remarkMath]}
|
// remarkPlugins={[remarkMath]}
|
||||||
rehypePlugins={[rehypeKatex]}
|
// rehypePlugins={[rehypeKatex]}
|
||||||
components={{ code: CodeBlock }}
|
// components={{ code: CodeBlock }}
|
||||||
>
|
// >
|
||||||
{markdownRAW}
|
// {markdownRAW}
|
||||||
</ReactMarkdown>
|
// </ReactMarkdown>
|
||||||
|
//
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
export default MarkdownToHTML;
|
// export default MarkdownToHTML;
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 497 KiB After Width: | Height: | Size: 0 B |
Reference in New Issue
Block a user