mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-07 22:38:14 +08:00
[dev]add markdown style
This commit is contained in:
parent
30a8f474ec
commit
a4239cac87
1652
package-lock.json
generated
1652
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,9 +18,12 @@
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.2.0",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
41
src/components/CodeBlock.jsx
Normal file
41
src/components/CodeBlock.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import SvgIcon from './SvgIcon';
|
||||
|
||||
/**
|
||||
* CodeBlock component renders a syntax highlighted code block with a copy button
|
||||
*/
|
||||
const CodeBlock = ({ language, value }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setCopied(true);
|
||||
// Reset the copied state after 2 seconds
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='code-block-container'>
|
||||
<div className='code-block-header'>
|
||||
{language && <span className='code-language-badge'>{language}</span>}
|
||||
<button onClick={handleCopy} className='copy-button' title={copied ? 'Copied!' : 'Copy code'}>
|
||||
{copied ? (
|
||||
<span className='copied-indicator'>✓ Copied!</span>
|
||||
) : (
|
||||
<span className='copy-icon'>
|
||||
<SvgIcon className='copy' width='16' height='16' />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<SyntaxHighlighter style={atomDark} language={language || 'text'} PreTag='div' wrapLongLines={true}>
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeBlock;
|
47
src/components/ErrorBoundary.jsx
Normal file
47
src/components/ErrorBoundary.jsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
/**
|
||||
* Error Boundary component to catch errors in child components
|
||||
* and display a fallback UI instead of crashing the whole app
|
||||
*/
|
||||
class ErrorBoundary extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// Update state so the next render will show the fallback UI
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
// You can log the error to an error reporting service
|
||||
console.error('Error caught by ErrorBoundary:', error);
|
||||
console.error('Component stack:', errorInfo.componentStack);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, fallback } = this.props;
|
||||
|
||||
if (this.state.hasError) {
|
||||
// You can render any custom fallback UI
|
||||
if (typeof fallback === 'function') {
|
||||
return fallback(this.state.error);
|
||||
}
|
||||
|
||||
return (
|
||||
fallback || (
|
||||
<div className='p-3 border rounded bg-light'>
|
||||
<p className='text-danger mb-1'>Error rendering content</p>
|
||||
<small className='text-muted'>The content couldn't be displayed properly.</small>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
50
src/components/SafeMarkdown.jsx
Normal file
50
src/components/SafeMarkdown.jsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
import CodeBlock from './CodeBlock';
|
||||
|
||||
/**
|
||||
* SafeMarkdown component that wraps ReactMarkdown with error handling
|
||||
* Displays raw content as fallback if markdown parsing fails
|
||||
*/
|
||||
const SafeMarkdown = ({ content, className = 'markdown-content' }) => {
|
||||
// Fallback UI that shows raw content when ReactMarkdown fails
|
||||
const renderFallback = (error) => {
|
||||
console.error('Markdown rendering error:', error);
|
||||
return (
|
||||
<div className={`${className} markdown-fallback`}>
|
||||
<p className='text-danger mb-2'>
|
||||
<small>Error rendering markdown. Showing raw content:</small>
|
||||
</p>
|
||||
<div className='p-2 border rounded'>{content}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallback={renderFallback}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
// Apply the className to the root element
|
||||
root: ({ node, ...props }) => <div className={className} {...props} />,
|
||||
code({ node, inline, className: codeClassName, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(codeClassName || '');
|
||||
return !inline && match ? (
|
||||
<CodeBlock language={match[1]} value={String(children).replace(/\n$/, '')} />
|
||||
) : (
|
||||
<code className={codeClassName} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default SafeMarkdown;
|
@ -6,6 +6,7 @@ import { showNotification } from '../../store/notification.slice';
|
||||
import { createChatRecord, fetchAvailableDatasets, fetchConversationDetail } from '../../store/chat/chat.thunks';
|
||||
import { fetchKnowledgeBases } from '../../store/knowledgeBase/knowledgeBase.thunks';
|
||||
import SvgIcon from '../../components/SvgIcon';
|
||||
import SafeMarkdown from '../../components/SafeMarkdown';
|
||||
import { get } from '../../services/api';
|
||||
|
||||
export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
@ -247,7 +248,13 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div className='message-content'>{message.content}</div>
|
||||
<div className='message-content'>
|
||||
{message.role === 'user' ? (
|
||||
message.content
|
||||
) : (
|
||||
<SafeMarkdown content={message.content} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='message-time small text-muted mt-1'>
|
||||
{message.created_at && new Date(message.created_at).toLocaleTimeString()}
|
||||
|
@ -8,7 +8,7 @@ import SvgIcon from '../../components/SvgIcon';
|
||||
export default function NewChat() {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [selectedDatasetId, setSelectedDatasetId] = useState(null);
|
||||
const [selectedDatasetIds, setSelectedDatasetIds] = useState([]);
|
||||
const [isNavigating, setIsNavigating] = useState(false);
|
||||
|
||||
// 从 Redux store 获取可用知识库数据
|
||||
@ -39,46 +39,89 @@ export default function NewChat() {
|
||||
}
|
||||
}, [error, dispatch]);
|
||||
|
||||
// 处理知识库选择
|
||||
const handleSelectKnowledgeBase = async (dataset) => {
|
||||
if (isNavigating) return; // 如果正在导航中,阻止重复点击
|
||||
// 处理知识库选择(切换选择状态)
|
||||
const handleToggleKnowledgeBase = (dataset) => {
|
||||
if (isNavigating) return; // 如果正在导航中,阻止操作
|
||||
|
||||
setSelectedDatasetIds((prev) => {
|
||||
// 检查是否已经选中
|
||||
const isSelected = prev.includes(dataset.id);
|
||||
|
||||
if (isSelected) {
|
||||
// 如果已选中,则从数组中移除
|
||||
return prev.filter((id) => id !== dataset.id);
|
||||
} else {
|
||||
// 如果未选中,则添加到数组中
|
||||
return [...prev, dataset.id];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 开始聊天
|
||||
const handleStartChat = async () => {
|
||||
if (selectedDatasetIds.length === 0) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '请至少选择一个知识库',
|
||||
type: 'warning',
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNavigating) return; // 防止重复点击
|
||||
|
||||
try {
|
||||
setSelectedDatasetId(dataset.id);
|
||||
setIsNavigating(true);
|
||||
|
||||
// 判断聊天历史中是否已存在该知识库的聊天记录
|
||||
// 检查是否已存在包含所有选中知识库的聊天记录
|
||||
// 注意:这里的逻辑简化了,实际可能需要更复杂的匹配算法
|
||||
const existingChat = chatHistory.find((chat) => {
|
||||
// 检查知识库ID是否匹配
|
||||
// 检查聊天记录中的知识库是否完全匹配当前选择
|
||||
if (chat.datasets && Array.isArray(chat.datasets)) {
|
||||
return chat.datasets.some((ds) => ds.id === dataset.id);
|
||||
const chatDatasetIds = chat.datasets.map((ds) => ds.id);
|
||||
return (
|
||||
chatDatasetIds.length === selectedDatasetIds.length &&
|
||||
selectedDatasetIds.every((id) => chatDatasetIds.includes(id))
|
||||
);
|
||||
}
|
||||
|
||||
// 兼容旧格式
|
||||
if (chat.dataset_id_list && Array.isArray(chat.dataset_id_list)) {
|
||||
return chat.dataset_id_list.includes(dataset.id.replace(/-/g, ''));
|
||||
const formattedSelectedIds = selectedDatasetIds.map((id) => id.replace(/-/g, ''));
|
||||
return (
|
||||
chat.dataset_id_list.length === formattedSelectedIds.length &&
|
||||
formattedSelectedIds.every((id) => chat.dataset_id_list.includes(id))
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (existingChat) {
|
||||
// 找到现有聊天记录,导航到该聊天页面
|
||||
console.log(`找到现有聊天记录,直接导航到 /chat/${dataset.id}/${existingChat.conversation_id}`);
|
||||
navigate(`/chat/${dataset.id}/${existingChat.conversation_id}`);
|
||||
// 使用第一个知识库ID作为URL参数
|
||||
const primaryDatasetId = selectedDatasetIds[0];
|
||||
console.log(`找到现有聊天记录,直接导航到 /chat/${primaryDatasetId}/${existingChat.conversation_id}`);
|
||||
navigate(`/chat/${primaryDatasetId}/${existingChat.conversation_id}`);
|
||||
} else {
|
||||
// 没有找到现有聊天记录,直接创建新的聊天(而不是导航到 /chat/${dataset.id})
|
||||
console.log(`未找到现有聊天记录,直接创建新的聊天,知识库ID: ${dataset.id}`);
|
||||
// 没有找到现有聊天记录,创建新的聊天
|
||||
console.log(`未找到现有聊天记录,直接创建新的聊天,选中的知识库ID: ${selectedDatasetIds.join(', ')}`);
|
||||
|
||||
// 直接在这里创建聊天记录,避免在 Chat.jsx 中再次创建
|
||||
// 创建新的聊天记录
|
||||
const formattedIds = selectedDatasetIds.map((id) => id.replace(/-/g, ''));
|
||||
const response = await dispatch(
|
||||
createChatRecord({
|
||||
dataset_id_list: [dataset.id.replace(/-/g, '')],
|
||||
dataset_id_list: formattedIds,
|
||||
question: '选择当前知识库,创建聊天',
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
if (response && response.conversation_id) {
|
||||
console.log(`创建成功,导航到 /chat/${dataset.id}/${response.conversation_id}`);
|
||||
navigate(`/chat/${dataset.id}/${response.conversation_id}`);
|
||||
// 使用第一个知识库ID作为URL参数
|
||||
const primaryDatasetId = selectedDatasetIds[0];
|
||||
console.log(`创建成功,导航到 /chat/${primaryDatasetId}/${response.conversation_id}`);
|
||||
navigate(`/chat/${primaryDatasetId}/${response.conversation_id}`);
|
||||
} else {
|
||||
throw new Error('未能获取会话ID');
|
||||
}
|
||||
@ -92,7 +135,6 @@ export default function NewChat() {
|
||||
})
|
||||
);
|
||||
setIsNavigating(false);
|
||||
setSelectedDatasetId(null);
|
||||
}
|
||||
};
|
||||
|
||||
@ -127,47 +169,63 @@ export default function NewChat() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h4 className='mb-4'>选择知识库开始聊天</h4>
|
||||
<div className='d-flex justify-content-between align-items-center mb-4'>
|
||||
<h4 className='m-0'>选择知识库开始聊天</h4>
|
||||
<button
|
||||
className='btn btn-dark'
|
||||
onClick={handleStartChat}
|
||||
disabled={selectedDatasetIds.length === 0 || isNavigating}
|
||||
>
|
||||
{isNavigating ? (
|
||||
<>
|
||||
<span
|
||||
className='spinner-border spinner-border-sm me-2'
|
||||
role='status'
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
加载中...
|
||||
</>
|
||||
) : (
|
||||
<>开始聊天 {selectedDatasetIds.length > 0 && `(${selectedDatasetIds.length})`}</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{selectedDatasetIds.length > 0 && (
|
||||
<div className='alert alert-dark mb-4'>已选择 {selectedDatasetIds.length} 个知识库</div>
|
||||
)}
|
||||
|
||||
<div className='row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 justify-content-center'>
|
||||
{datasets.length > 0 ? (
|
||||
datasets.map((dataset) => (
|
||||
<div key={dataset.id} className='col'>
|
||||
<div
|
||||
className={`card h-100 shadow-sm border-0 ${!isNavigating ? 'cursor-pointer' : ''} ${
|
||||
selectedDatasetId === dataset.id ? 'border-primary' : ''
|
||||
}`}
|
||||
onClick={() => handleSelectKnowledgeBase(dataset)}
|
||||
style={{ opacity: isNavigating && selectedDatasetId !== dataset.id ? 0.6 : 1 }}
|
||||
>
|
||||
<div className='card-body'>
|
||||
<h5 className='card-title d-flex justify-content-between align-items-center'>
|
||||
{dataset.name}
|
||||
{selectedDatasetId === dataset.id && isNavigating && (
|
||||
<div
|
||||
className='spinner-border spinner-border-sm text-primary'
|
||||
role='status'
|
||||
>
|
||||
<span className='visually-hidden'>加载中...</span>
|
||||
</div>
|
||||
)}
|
||||
</h5>
|
||||
<p className='card-text text-muted'>{dataset.desc || dataset.description || ''}</p>
|
||||
<div className='text-muted small d-flex align-items-center gap-2'>
|
||||
<span className='d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='file' />
|
||||
{dataset.document_count || 0} 文档
|
||||
</span>
|
||||
<span className='d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='clock' />
|
||||
{dataset.create_time
|
||||
? new Date(dataset.create_time).toLocaleDateString()
|
||||
: 'N/A'}
|
||||
</span>
|
||||
datasets.map((dataset) => {
|
||||
const isSelected = selectedDatasetIds.includes(dataset.id);
|
||||
return (
|
||||
<div key={dataset.id} className='col'>
|
||||
<div
|
||||
className={`card h-100 shadow-sm ${!isNavigating ? 'cursor-pointer' : ''} ${
|
||||
isSelected ? 'border-gray border-2' : 'border-0'
|
||||
}`}
|
||||
onClick={() => handleToggleKnowledgeBase(dataset)}
|
||||
style={{ opacity: isNavigating && !isSelected ? 0.6 : 1 }}
|
||||
>
|
||||
<div className='card-body'>
|
||||
<h5 className='card-title d-flex justify-content-between align-items-center'>
|
||||
{dataset.name}
|
||||
{isSelected && <SvgIcon className='check-circle text-primary' />}
|
||||
</h5>
|
||||
<p className='card-text text-muted'>
|
||||
{dataset.desc || dataset.description || ''}
|
||||
</p>
|
||||
<div className='text-muted small d-flex align-items-center gap-2'>
|
||||
<span className='d-flex align-items-center gap-1'>
|
||||
{dataset.department || ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className='col-12'>
|
||||
<div className='alert alert-warning'>暂无可访问的知识库,请先申请知识库访问权限</div>
|
||||
|
@ -11,6 +11,134 @@
|
||||
top: 6.5rem;
|
||||
}
|
||||
|
||||
/* Markdown styling in chat messages */
|
||||
.markdown-content {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
color: inherit;
|
||||
|
||||
/* Heading styles */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 { font-size: 1.5rem; }
|
||||
h2 { font-size: 1.35rem; }
|
||||
h3 { font-size: 1.2rem; }
|
||||
h4 { font-size: 1.1rem; }
|
||||
h5, h6 { font-size: 1rem; }
|
||||
|
||||
/* Paragraph spacing */
|
||||
p {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul, ol {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* Code blocks with syntax highlighting */
|
||||
pre, pre.prism-code {
|
||||
margin: 0.5rem 0 !important;
|
||||
padding: 0.75rem !important;
|
||||
border-radius: 0.375rem !important;
|
||||
font-size: 0.85rem !important;
|
||||
line-height: 1.5 !important;
|
||||
|
||||
/* Improve readability on dark background */
|
||||
code span {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add copy button positioning for future enhancements */
|
||||
.code-block-container {
|
||||
position: relative;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
code {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 0.15rem 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Block quotes */
|
||||
blockquote {
|
||||
border-left: 3px solid #dee2e6;
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
margin-bottom: 0.75rem;
|
||||
border-collapse: collapse;
|
||||
|
||||
th, td {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
|
||||
/* Images */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Horizontal rule */
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
border: 0;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply different text colors based on message background */
|
||||
.bg-dark .markdown-content {
|
||||
color: white;
|
||||
|
||||
code {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left-color: rgba(255, 255, 255, 0.3);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8bb9fe;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-card {
|
||||
min-width: 20rem;
|
||||
cursor: pointer;
|
||||
@ -144,3 +272,78 @@
|
||||
.dark-select {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='%23000' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
/* Code Block Styles */
|
||||
.code-block-container {
|
||||
position: relative;
|
||||
margin: 0.75rem 0;
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
background-color: #282c34; /* Dark background matching atomDark theme */
|
||||
}
|
||||
|
||||
.code-block-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.375rem 0.75rem;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.code-language-badge {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
padding: 0.125rem 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.copied-indicator {
|
||||
color: #10b981; /* Green color for success */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* Override the default SyntaxHighlighter styles */
|
||||
.code-block-container pre {
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important; /* Remove rounded corners inside the container */
|
||||
}
|
||||
|
||||
/* Markdown fallback styling */
|
||||
.markdown-fallback {
|
||||
font-size: 0.95rem;
|
||||
|
||||
.text-danger {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user