修补输入框
This commit is contained in:
@@ -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 (
|
||||
<div key={message.floor} className={`message ${isUser ? 'user' : 'ai'}`}>
|
||||
<div className="message-container">
|
||||
<div className="message-header">
|
||||
<span className="message-name">{displayName}</span>
|
||||
<span className="message-id">#{message.floor}</span>
|
||||
<div className="message-toolbar">
|
||||
<div className="toolbar-buttons">
|
||||
<button
|
||||
className="toolbar-button"
|
||||
onClick={() => handleEdit(message)}
|
||||
title="编辑"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
className="toolbar-button"
|
||||
title="更多"
|
||||
>
|
||||
•••
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="message-content">
|
||||
{isEditing ? (
|
||||
<div className="edit-container">
|
||||
<textarea
|
||||
className="edit-textarea"
|
||||
value={editContent}
|
||||
onChange={(e) => setEditContent(e.target.value)}
|
||||
/>
|
||||
<div className="edit-buttons">
|
||||
<button
|
||||
className="cancel-button"
|
||||
onClick={handleCancelEdit}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="save-button"
|
||||
onClick={() => handleSaveEdit(message.floor)}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bubble">
|
||||
{message.mes}
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div key={message.floor} className={`message ${isUser ? 'user' : 'ai'}`}>
|
||||
<div className="message-container">
|
||||
<div className="message-header">
|
||||
<span className="message-name">{displayName}</span>
|
||||
<span className="message-id">#{message.floor}</span>
|
||||
<div className="message-toolbar">
|
||||
<div className="toolbar-buttons">
|
||||
<button
|
||||
className="toolbar-button"
|
||||
onClick={() => handleEdit(message)}
|
||||
title="编辑"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
className="toolbar-button"
|
||||
title="更多"
|
||||
>
|
||||
•••
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="message-content">
|
||||
{isEditing ? (
|
||||
<div className="edit-container">
|
||||
<textarea
|
||||
className="edit-textarea"
|
||||
value={editContent}
|
||||
onChange={(e) => setEditContent(e.target.value)}
|
||||
/>
|
||||
<div className="edit-buttons">
|
||||
<button
|
||||
className="cancel-button"
|
||||
onClick={handleCancelEdit}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="save-button"
|
||||
onClick={() => handleSaveEdit(message.floor)}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bubble">
|
||||
{message.mes}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-box">
|
||||
<div className="chat-messages">
|
||||
{isLoading ? (
|
||||
<div className="loading">加载中...</div>
|
||||
) : error ? (
|
||||
<div className="error">{error}</div>
|
||||
) : messages.length === 0 ? (
|
||||
<div className="loading">暂无消息</div>
|
||||
) : (
|
||||
messages.map(renderMessage)
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-box">
|
||||
<div className="chat-messages">
|
||||
{isLoading ? (
|
||||
<div className="loading">加载中...</div>
|
||||
) : error ? (
|
||||
<div className="error">{error}</div>
|
||||
) : messages.length === 0 ? (
|
||||
<div className="loading">暂无消息</div>
|
||||
) : (
|
||||
messages.map(renderMessage)
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatBox;
|
||||
|
||||
@@ -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 = () => (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24">
|
||||
<path d="M7 10l5 5 5-5z" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CopySvg = () => (
|
||||
<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" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
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(`<pre>Error rendering mermaid: ${err}</pre>`);
|
||||
}
|
||||
}, [code, id]);
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: svg }} />;
|
||||
};
|
||||
|
||||
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 <code className={className} {...props}>{children}</code>;
|
||||
}
|
||||
|
||||
// 处理 Mermaid 图表代码
|
||||
if (match?.[1] === "mermaid" && isMermaidLoaded) {
|
||||
return (
|
||||
<div style={{ position: "relative", margin: "20px 0" }}>
|
||||
<div className="code-header">
|
||||
<div
|
||||
style={{ cursor: "pointer", marginRight: "10px", transformOrigin: "8px" }}
|
||||
className={isShowCode ? "code-rotate-down" : "code-rotate-right"}
|
||||
onClick={() => setIsShowCode(!isShowCode)}
|
||||
>
|
||||
<DownSvg />
|
||||
</div>
|
||||
<div>{match[1]}</div>
|
||||
<CopyToClipboard text={codeContent} onCopy={handleCopy}>
|
||||
<div className="preview-code-copy" style={{ cursor: "pointer" }}>
|
||||
{isShowCopy && <span className="copy-success">✓ 复制成功</span>}
|
||||
<CopySvg />
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{isShowCode && <MermaidChart code={codeContent} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative", padding:"0"}}>
|
||||
<div className="code-header">
|
||||
<div
|
||||
style={{ cursor: "pointer", marginRight: "10px", transformOrigin: "8px" }}
|
||||
className={isShowCode ? "code-rotate-down" : "code-rotate-right"}
|
||||
onClick={() => setIsShowCode(!isShowCode)}
|
||||
>
|
||||
<DownSvg style={{
|
||||
transform: isShowCode ? "rotate(0deg)" : "rotate(-90deg)",
|
||||
transition: "transform 0.2s"
|
||||
}} />
|
||||
</div>
|
||||
<div>{match?.[1] || "code"}</div>
|
||||
<CopyToClipboard text={codeContent} onCopy={handleCopy}>
|
||||
<div className="preview-code-copy" style={{ cursor: "pointer" }}>
|
||||
{isShowCopy && <span className="copy-success">✓ 复制成功</span>}
|
||||
<CopySvg />
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{isShowCode && (
|
||||
<SyntaxHighlighter
|
||||
style={materialLight}
|
||||
language={match?.[1] || "text"}
|
||||
PreTag="div"
|
||||
showLineNumbers
|
||||
{...props}
|
||||
>
|
||||
{codeContent}
|
||||
</SyntaxHighlighter>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[isMermaidLoaded]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className} >
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
components={{ code: CodeBlock }}
|
||||
>
|
||||
{markdownRAW}
|
||||
</ReactMarkdown>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownToHTML;
|
||||
// // 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 = () => (
|
||||
// <svg width="12" height="12" viewBox="0 0 24 24">
|
||||
// <path d="M7 10l5 5 5-5z" fill="currentColor" />
|
||||
// </svg>
|
||||
// );
|
||||
//
|
||||
// const CopySvg = () => (
|
||||
// <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" />
|
||||
// </svg>
|
||||
// );
|
||||
//
|
||||
//
|
||||
// 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(`<pre>Error rendering mermaid: ${err}</pre>`);
|
||||
// }
|
||||
// }, [code, id]);
|
||||
//
|
||||
// return <div dangerouslySetInnerHTML={{ __html: svg }} />;
|
||||
// };
|
||||
//
|
||||
// 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 <code className={className} {...props}>{children}</code>;
|
||||
// }
|
||||
//
|
||||
// // 处理 Mermaid 图表代码
|
||||
// if (match?.[1] === "mermaid" && isMermaidLoaded) {
|
||||
// return (
|
||||
// <div style={{ position: "relative", margin: "20px 0" }}>
|
||||
// <div className="code-header">
|
||||
// <div
|
||||
// style={{ cursor: "pointer", marginRight: "10px", transformOrigin: "8px" }}
|
||||
// className={isShowCode ? "code-rotate-down" : "code-rotate-right"}
|
||||
// onClick={() => setIsShowCode(!isShowCode)}
|
||||
// >
|
||||
// <DownSvg />
|
||||
// </div>
|
||||
// <div>{match[1]}</div>
|
||||
// <CopyToClipboard text={codeContent} onCopy={handleCopy}>
|
||||
// <div className="preview-code-copy" style={{ cursor: "pointer" }}>
|
||||
// {isShowCopy && <span className="copy-success">✓ 复制成功</span>}
|
||||
// <CopySvg />
|
||||
// </div>
|
||||
// </CopyToClipboard>
|
||||
// </div>
|
||||
// {isShowCode && <MermaidChart code={codeContent} />}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// return (
|
||||
// <div style={{ position: "relative", padding:"0"}}>
|
||||
// <div className="code-header">
|
||||
// <div
|
||||
// style={{ cursor: "pointer", marginRight: "10px", transformOrigin: "8px" }}
|
||||
// className={isShowCode ? "code-rotate-down" : "code-rotate-right"}
|
||||
// onClick={() => setIsShowCode(!isShowCode)}
|
||||
// >
|
||||
// <DownSvg style={{
|
||||
// transform: isShowCode ? "rotate(0deg)" : "rotate(-90deg)",
|
||||
// transition: "transform 0.2s"
|
||||
// }} />
|
||||
// </div>
|
||||
// <div>{match?.[1] || "code"}</div>
|
||||
// <CopyToClipboard text={codeContent} onCopy={handleCopy}>
|
||||
// <div className="preview-code-copy" style={{ cursor: "pointer" }}>
|
||||
// {isShowCopy && <span className="copy-success">✓ 复制成功</span>}
|
||||
// <CopySvg />
|
||||
// </div>
|
||||
// </CopyToClipboard>
|
||||
// </div>
|
||||
// {isShowCode && (
|
||||
// <SyntaxHighlighter
|
||||
// style={materialLight}
|
||||
// language={match?.[1] || "text"}
|
||||
// PreTag="div"
|
||||
// showLineNumbers
|
||||
// {...props}
|
||||
// >
|
||||
// {codeContent}
|
||||
// </SyntaxHighlighter>
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
// [isMermaidLoaded]
|
||||
// );
|
||||
//
|
||||
// return (
|
||||
// <div
|
||||
// className={className} >
|
||||
// <ReactMarkdown
|
||||
// remarkPlugins={[remarkMath]}
|
||||
// rehypePlugins={[rehypeKatex]}
|
||||
// components={{ code: CodeBlock }}
|
||||
// >
|
||||
// {markdownRAW}
|
||||
// </ReactMarkdown>
|
||||
//
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
//
|
||||
// export default MarkdownToHTML;
|
||||
Reference in New Issue
Block a user