mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 05:09:44 +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",
|
"lodash": "^4.17.21",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.2.0",
|
"react-router-dom": "^7.2.0",
|
||||||
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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 { createChatRecord, fetchAvailableDatasets, fetchConversationDetail } from '../../store/chat/chat.thunks';
|
||||||
import { fetchKnowledgeBases } from '../../store/knowledgeBase/knowledgeBase.thunks';
|
import { fetchKnowledgeBases } from '../../store/knowledgeBase/knowledgeBase.thunks';
|
||||||
import SvgIcon from '../../components/SvgIcon';
|
import SvgIcon from '../../components/SvgIcon';
|
||||||
|
import SafeMarkdown from '../../components/SafeMarkdown';
|
||||||
import { get } from '../../services/api';
|
import { get } from '../../services/api';
|
||||||
|
|
||||||
export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||||
@ -247,7 +248,13 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
position: 'relative',
|
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>
|
||||||
<div className='message-time small text-muted mt-1'>
|
<div className='message-time small text-muted mt-1'>
|
||||||
{message.created_at && new Date(message.created_at).toLocaleTimeString()}
|
{message.created_at && new Date(message.created_at).toLocaleTimeString()}
|
||||||
|
@ -8,7 +8,7 @@ import SvgIcon from '../../components/SvgIcon';
|
|||||||
export default function NewChat() {
|
export default function NewChat() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [selectedDatasetId, setSelectedDatasetId] = useState(null);
|
const [selectedDatasetIds, setSelectedDatasetIds] = useState([]);
|
||||||
const [isNavigating, setIsNavigating] = useState(false);
|
const [isNavigating, setIsNavigating] = useState(false);
|
||||||
|
|
||||||
// 从 Redux store 获取可用知识库数据
|
// 从 Redux store 获取可用知识库数据
|
||||||
@ -39,46 +39,89 @@ export default function NewChat() {
|
|||||||
}
|
}
|
||||||
}, [error, dispatch]);
|
}, [error, dispatch]);
|
||||||
|
|
||||||
// 处理知识库选择
|
// 处理知识库选择(切换选择状态)
|
||||||
const handleSelectKnowledgeBase = async (dataset) => {
|
const handleToggleKnowledgeBase = (dataset) => {
|
||||||
if (isNavigating) return; // 如果正在导航中,阻止重复点击
|
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 {
|
try {
|
||||||
setSelectedDatasetId(dataset.id);
|
|
||||||
setIsNavigating(true);
|
setIsNavigating(true);
|
||||||
|
|
||||||
// 判断聊天历史中是否已存在该知识库的聊天记录
|
// 检查是否已存在包含所有选中知识库的聊天记录
|
||||||
|
// 注意:这里的逻辑简化了,实际可能需要更复杂的匹配算法
|
||||||
const existingChat = chatHistory.find((chat) => {
|
const existingChat = chatHistory.find((chat) => {
|
||||||
// 检查知识库ID是否匹配
|
// 检查聊天记录中的知识库是否完全匹配当前选择
|
||||||
if (chat.datasets && Array.isArray(chat.datasets)) {
|
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)) {
|
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;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingChat) {
|
if (existingChat) {
|
||||||
// 找到现有聊天记录,导航到该聊天页面
|
// 找到现有聊天记录,导航到该聊天页面
|
||||||
console.log(`找到现有聊天记录,直接导航到 /chat/${dataset.id}/${existingChat.conversation_id}`);
|
// 使用第一个知识库ID作为URL参数
|
||||||
navigate(`/chat/${dataset.id}/${existingChat.conversation_id}`);
|
const primaryDatasetId = selectedDatasetIds[0];
|
||||||
|
console.log(`找到现有聊天记录,直接导航到 /chat/${primaryDatasetId}/${existingChat.conversation_id}`);
|
||||||
|
navigate(`/chat/${primaryDatasetId}/${existingChat.conversation_id}`);
|
||||||
} else {
|
} 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(
|
const response = await dispatch(
|
||||||
createChatRecord({
|
createChatRecord({
|
||||||
dataset_id_list: [dataset.id.replace(/-/g, '')],
|
dataset_id_list: formattedIds,
|
||||||
question: '选择当前知识库,创建聊天',
|
question: '选择当前知识库,创建聊天',
|
||||||
})
|
})
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
if (response && response.conversation_id) {
|
if (response && response.conversation_id) {
|
||||||
console.log(`创建成功,导航到 /chat/${dataset.id}/${response.conversation_id}`);
|
// 使用第一个知识库ID作为URL参数
|
||||||
navigate(`/chat/${dataset.id}/${response.conversation_id}`);
|
const primaryDatasetId = selectedDatasetIds[0];
|
||||||
|
console.log(`创建成功,导航到 /chat/${primaryDatasetId}/${response.conversation_id}`);
|
||||||
|
navigate(`/chat/${primaryDatasetId}/${response.conversation_id}`);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('未能获取会话ID');
|
throw new Error('未能获取会话ID');
|
||||||
}
|
}
|
||||||
@ -92,7 +135,6 @@ export default function NewChat() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
setIsNavigating(false);
|
setIsNavigating(false);
|
||||||
setSelectedDatasetId(null);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -127,47 +169,63 @@ export default function NewChat() {
|
|||||||
</div>
|
</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'>
|
<div className='row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 justify-content-center'>
|
||||||
{datasets.length > 0 ? (
|
{datasets.length > 0 ? (
|
||||||
datasets.map((dataset) => (
|
datasets.map((dataset) => {
|
||||||
|
const isSelected = selectedDatasetIds.includes(dataset.id);
|
||||||
|
return (
|
||||||
<div key={dataset.id} className='col'>
|
<div key={dataset.id} className='col'>
|
||||||
<div
|
<div
|
||||||
className={`card h-100 shadow-sm border-0 ${!isNavigating ? 'cursor-pointer' : ''} ${
|
className={`card h-100 shadow-sm ${!isNavigating ? 'cursor-pointer' : ''} ${
|
||||||
selectedDatasetId === dataset.id ? 'border-primary' : ''
|
isSelected ? 'border-gray border-2' : 'border-0'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleSelectKnowledgeBase(dataset)}
|
onClick={() => handleToggleKnowledgeBase(dataset)}
|
||||||
style={{ opacity: isNavigating && selectedDatasetId !== dataset.id ? 0.6 : 1 }}
|
style={{ opacity: isNavigating && !isSelected ? 0.6 : 1 }}
|
||||||
>
|
>
|
||||||
<div className='card-body'>
|
<div className='card-body'>
|
||||||
<h5 className='card-title d-flex justify-content-between align-items-center'>
|
<h5 className='card-title d-flex justify-content-between align-items-center'>
|
||||||
{dataset.name}
|
{dataset.name}
|
||||||
{selectedDatasetId === dataset.id && isNavigating && (
|
{isSelected && <SvgIcon className='check-circle text-primary' />}
|
||||||
<div
|
|
||||||
className='spinner-border spinner-border-sm text-primary'
|
|
||||||
role='status'
|
|
||||||
>
|
|
||||||
<span className='visually-hidden'>加载中...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</h5>
|
</h5>
|
||||||
<p className='card-text text-muted'>{dataset.desc || dataset.description || ''}</p>
|
<p className='card-text text-muted'>
|
||||||
|
{dataset.desc || dataset.description || ''}
|
||||||
|
</p>
|
||||||
<div className='text-muted small d-flex align-items-center gap-2'>
|
<div className='text-muted small d-flex align-items-center gap-2'>
|
||||||
<span className='d-flex align-items-center gap-1'>
|
<span className='d-flex align-items-center gap-1'>
|
||||||
<SvgIcon className='file' />
|
{dataset.department || ''}
|
||||||
{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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className='col-12'>
|
<div className='col-12'>
|
||||||
<div className='alert alert-warning'>暂无可访问的知识库,请先申请知识库访问权限</div>
|
<div className='alert alert-warning'>暂无可访问的知识库,请先申请知识库访问权限</div>
|
||||||
|
@ -11,6 +11,134 @@
|
|||||||
top: 6.5rem;
|
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 {
|
.knowledge-card {
|
||||||
min-width: 20rem;
|
min-width: 20rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -144,3 +272,78 @@
|
|||||||
.dark-select {
|
.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");
|
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