Merge pull request #10 from Funkoala14/dev

[dev]ws setting
This commit is contained in:
Susie Shi 2025-04-11 12:27:53 -04:00 committed by GitHub
commit 8af2792bb5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 206 additions and 114 deletions

View File

@ -25,14 +25,14 @@ function App() {
// WebSocket // WebSocket
if (user && !isConnected) { if (user && !isConnected) {
// initWebSocket() initWebSocket()
// .then(() => { .then(() => {
// dispatch(setWebSocketConnected(true)); dispatch(setWebSocketConnected(true));
// console.log('WebSocket connection initialized'); console.log('WebSocket connection initialized');
// }) })
// .catch((error) => { .catch((error) => {
// console.error('Failed to initialize WebSocket connection:', error); console.error('Failed to initialize WebSocket connection:', error);
// }); });
} }
// WebSocket // WebSocket

View File

@ -15,6 +15,7 @@ import SvgIcon from './SvgIcon';
* @param {boolean} props.isSearchLoading - 搜索是否正在加载 * @param {boolean} props.isSearchLoading - 搜索是否正在加载
* @param {Function} props.onResultClick - 点击搜索结果的回调 * @param {Function} props.onResultClick - 点击搜索结果的回调
* @param {Function} props.onRequestAccess - 申请权限的回调 * @param {Function} props.onRequestAccess - 申请权限的回调
* @param {string} props.cornerStyle - 设置圆角风格可选值: 'rounded'(圆角) 'square'(方角)
*/ */
const SearchBar = ({ const SearchBar = ({
searchKeyword, searchKeyword,
@ -22,17 +23,23 @@ const SearchBar = ({
onSearchChange, onSearchChange,
onSearch, onSearch,
onClearSearch, onClearSearch,
placeholder = '搜索...', placeholder = '搜索知识库...',
className = 'w-50', className = 'w-50',
searchResults = [], searchResults = [],
isSearchLoading = false, isSearchLoading = false,
onResultClick, onResultClick,
onRequestAccess, onRequestAccess,
cornerStyle = 'rounded', //
}) => { }) => {
const [showDropdown, setShowDropdown] = useState(false); const [showDropdown, setShowDropdown] = useState(false);
const searchRef = useRef(null); const searchRef = useRef(null);
const inputRef = useRef(null); const inputRef = useRef(null);
//
const getBorderRadiusClass = () => {
return cornerStyle === 'rounded' ? 'rounded-pill' : 'rounded-0';
};
// //
useEffect(() => { useEffect(() => {
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
@ -69,11 +76,11 @@ const SearchBar = ({
return ( return (
<div className={`position-relative ${className}`} ref={searchRef}> <div className={`position-relative ${className}`} ref={searchRef}>
<form className='d-flex' onSubmit={handleSubmit}> <form className='d-flex' onSubmit={handleSubmit}>
<div className='input-group'> <div className={`input-group search-input-group ${getBorderRadiusClass()}`}>
<input <input
ref={inputRef} ref={inputRef}
type='text' type='text'
className='form-control' className={`form-control search-input border-end-0 ${getBorderRadiusClass()}`}
placeholder={placeholder} placeholder={placeholder}
value={searchKeyword} value={searchKeyword}
onChange={handleInputChange} onChange={handleInputChange}
@ -81,7 +88,9 @@ const SearchBar = ({
{searchKeyword.trim() && ( {searchKeyword.trim() && (
<button <button
type='button' type='button'
className='btn btn-outline-secondary border-start-0' className={`btn btn-outline-secondary border-start-0 ${
cornerStyle === 'rounded' ? '' : 'rounded-0'
}`}
onClick={() => { onClick={() => {
onClearSearch(); onClearSearch();
setShowDropdown(false); setShowDropdown(false);
@ -91,7 +100,12 @@ const SearchBar = ({
<SvgIcon className='close' /> <SvgIcon className='close' />
</button> </button>
)} )}
<button type='submit' className='btn btn-outline-secondary'> <button
type='submit'
className={`btn btn-outline-secondary search-button ${
cornerStyle === 'rounded' ? 'rounded-end-pill' : 'rounded-0'
}`}
>
<SvgIcon className='search' /> <SvgIcon className='search' />
</button> </button>
</div> </div>
@ -99,7 +113,11 @@ const SearchBar = ({
{/* 搜索结果下拉框 - 仅在用户搜索且有结果时显示 */} {/* 搜索结果下拉框 - 仅在用户搜索且有结果时显示 */}
{showDropdown && (isSearchLoading || searchResults?.length > 0) && ( {showDropdown && (isSearchLoading || searchResults?.length > 0) && (
<div className='position-absolute bg-white shadow-sm rounded-3 mt-1 w-100 search-results-dropdown z-1'> <div
className={`position-absolute bg-white shadow-sm mt-1 w-100 search-results-dropdown z-1 ${
cornerStyle === 'rounded' ? 'rounded-3' : ''
}`}
>
<div className='p-2 overflow-auto' style={{ maxHeight: '350px', zIndex: '1050' }}> <div className='p-2 overflow-auto' style={{ maxHeight: '350px', zIndex: '1050' }}>
{isSearchLoading ? ( {isSearchLoading ? (
<div className='text-center p-3'> <div className='text-center p-3'>
@ -116,7 +134,9 @@ const SearchBar = ({
{searchResults.map((item) => ( {searchResults.map((item) => (
<div <div
key={item.id} key={item.id}
className='search-result-item p-2 rounded-2 mb-1 hover-bg-light' className={`search-result-item p-2 mb-1 hover-bg-light ${
cornerStyle === 'rounded' ? 'rounded-2' : ''
}`}
style={{ style={{
cursor: item.permissions?.can_read ? 'pointer' : 'default', cursor: item.permissions?.can_read ? 'pointer' : 'default',
}} }}
@ -167,7 +187,9 @@ const SearchBar = ({
</div> </div>
{!item.permissions?.can_read && ( {!item.permissions?.can_read && (
<button <button
className='btn btn-sm btn-outline-primary ms-2' className={`btn btn-sm btn-outline-primary ms-2 ${
cornerStyle === 'rounded' ? '' : 'rounded-0'
}`}
onClick={() => { onClick={() => {
onRequestAccess(item.id, item.name); onRequestAccess(item.id, item.name);
setShowDropdown(false); setShowDropdown(false);

View File

@ -79,7 +79,7 @@ export default function HeaderWithNav() {
</ul> </ul>
{!!user ? ( {!!user ? (
<div className='d-flex align-items-center gap-3'> <div className='d-flex align-items-center gap-3'>
{/* <div className='position-relative'> <div className='position-relative'>
<button <button
className='btn btn-link text-dark p-0' className='btn btn-link text-dark p-0'
onClick={() => setShowNotifications(!showNotifications)} onClick={() => setShowNotifications(!showNotifications)}
@ -102,7 +102,7 @@ export default function HeaderWithNav() {
</span> </span>
)} )}
</button> </button>
</div> */} </div>
<div className='flex-shrink-0 dropdown'> <div className='flex-shrink-0 dropdown'>
<a <a
href='#' href='#'

View File

@ -9,8 +9,10 @@ const WS_BASE_URL = API_URL.replace(/^http/, 'ws').replace(/\/api\/?$/, '');
let socket = null; let socket = null;
let reconnectTimer = null; let reconnectTimer = null;
let pingInterval = null; let pingInterval = null;
let reconnectAttempts = 0; // 添加重连尝试计数器
const RECONNECT_DELAY = 5000; // 5秒后尝试重连 const RECONNECT_DELAY = 5000; // 5秒后尝试重连
const PING_INTERVAL = 30000; // 30秒发送一次ping const PING_INTERVAL = 30000; // 30秒发送一次ping
const MAX_RECONNECT_ATTEMPTS = 3; // 最大重连尝试次数
/** /**
* 初始化WebSocket连接 * 初始化WebSocket连接
@ -37,11 +39,13 @@ export const initWebSocket = () => {
} }
const wsUrl = `${WS_BASE_URL}/ws/notifications?token=${encryptedToken}`; const wsUrl = `${WS_BASE_URL}/ws/notifications?token=${encryptedToken}`;
console.log('WebSocket URL:', wsUrl);
socket = new WebSocket(wsUrl); socket = new WebSocket(wsUrl);
// 连接建立时的处理 // 连接建立时的处理
socket.onopen = () => { socket.onopen = () => {
console.log('WebSocket connection established'); console.log('WebSocket connection established');
reconnectAttempts = 0; // 连接成功后重置重连计数器
// 订阅通知频道 // 订阅通知频道
subscribeToNotifications(); subscribeToNotifications();
@ -81,12 +85,19 @@ export const initWebSocket = () => {
// 如果不是正常关闭,尝试重连 // 如果不是正常关闭,尝试重连
if (event.code !== 1000) { if (event.code !== 1000) {
reconnectTimer = setTimeout(() => { if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
console.log('Attempting to reconnect WebSocket...'); reconnectAttempts++;
initWebSocket().catch((err) => { console.log(`WebSocket reconnection attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`);
console.error('Failed to reconnect WebSocket:', err);
}); reconnectTimer = setTimeout(() => {
}, RECONNECT_DELAY); console.log('Attempting to reconnect WebSocket...');
initWebSocket().catch((err) => {
console.error('Failed to reconnect WebSocket:', err);
});
}, RECONNECT_DELAY);
} else {
console.log('Maximum reconnection attempts reached. Giving up.');
}
} }
}; };
} catch (error) { } catch (error) {

View File

@ -18,17 +18,33 @@
color: inherit; color: inherit;
/* Heading styles */ /* Heading styles */
h1, h2, h3, h4, h5, h6 { h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 1rem; margin-top: 1rem;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
font-weight: 600; font-weight: 600;
} }
h1 { font-size: 1.5rem; } h1 {
h2 { font-size: 1.35rem; } font-size: 1.5rem;
h3 { font-size: 1.2rem; } }
h4 { font-size: 1.1rem; } h2 {
h5, h6 { font-size: 1rem; } font-size: 1.35rem;
}
h3 {
font-size: 1.2rem;
}
h4 {
font-size: 1.1rem;
}
h5,
h6 {
font-size: 1rem;
}
/* Paragraph spacing */ /* Paragraph spacing */
p { p {
@ -36,19 +52,21 @@
} }
/* Lists */ /* Lists */
ul, ol { ul,
ol {
padding-left: 1.5rem; padding-left: 1.5rem;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
/* Code blocks with syntax highlighting */ /* Code blocks with syntax highlighting */
pre, pre.prism-code { pre,
pre.prism-code {
margin: 0.5rem 0 !important; margin: 0.5rem 0 !important;
padding: 0.75rem !important; padding: 0.75rem !important;
border-radius: 0.375rem !important; border-radius: 0.375rem !important;
font-size: 0.85rem !important; font-size: 0.85rem !important;
line-height: 1.5 !important; line-height: 1.5 !important;
/* Improve readability on dark background */ /* Improve readability on dark background */
code span { code span {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
@ -91,12 +109,13 @@
width: 100%; width: 100%;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
border-collapse: collapse; border-collapse: collapse;
th, td { th,
td {
padding: 0.5rem; padding: 0.5rem;
border: 1px solid #dee2e6; border: 1px solid #dee2e6;
} }
th { th {
background-color: #f8f9fa; background-color: #f8f9fa;
} }
@ -120,20 +139,20 @@
/* Apply different text colors based on message background */ /* Apply different text colors based on message background */
.bg-dark .markdown-content { .bg-dark .markdown-content {
color: white; color: white;
code { code {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
pre { pre {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
blockquote { blockquote {
border-left-color: rgba(255, 255, 255, 0.3); border-left-color: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
} }
a { a {
color: #8bb9fe; color: #8bb9fe;
} }
@ -142,8 +161,8 @@
.knowledge-card { .knowledge-card {
min-width: 20rem; min-width: 20rem;
cursor: pointer; cursor: pointer;
.hoverdown:hover .hoverdown-menu{ .hoverdown:hover .hoverdown-menu {
display: block; display: block;
color: red; color: red;
} }
@ -161,7 +180,7 @@
gap: 8px; gap: 8px;
border-radius: 4px; border-radius: 4px;
color: $dark; color: $dark;
&:hover { &:hover {
background-color: $gray-100; background-color: $gray-100;
} }
@ -178,41 +197,41 @@
/* 自定义黑色系开关样式 */ /* 自定义黑色系开关样式 */
.dark-switch .form-check-input { .dark-switch .form-check-input {
border: 1px solid #dee2e6; border: 1px solid #dee2e6;
background-color: #fff; /* 关闭状态背景色 */ background-color: #fff; /* 关闭状态背景色 */
} }
/* 关闭状态滑块 */ /* 关闭状态滑块 */
.dark-switch .form-check-input:not(:checked) { .dark-switch .form-check-input:not(:checked) {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle fill='%23adb5bd' r='3'/></svg>"); background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle fill='%23adb5bd' r='3'/></svg>");
} }
/* 打开状态 */ /* 打开状态 */
.dark-switch .form-check-input:checked { .dark-switch .form-check-input:checked {
background-color: #000; /* 打开状态背景色 */ background-color: #000; /* 打开状态背景色 */
border-color: #000; border-color: #000;
} }
/* 打开状态滑块 */ /* 打开状态滑块 */
.dark-switch .form-check-input:checked { .dark-switch .form-check-input:checked {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle fill='%23fff' r='3'/></svg>"); background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle fill='%23fff' r='3'/></svg>");
} }
/* 悬停效果 */ /* 悬停效果 */
.dark-switch .form-check-input:hover { .dark-switch .form-check-input:hover {
filter: brightness(0.9); filter: brightness(0.9);
} }
/* 禁用状态 */ /* 禁用状态 */
.dark-switch .form-check-input:disabled { .dark-switch .form-check-input:disabled {
opacity: 0.5; opacity: 0.5;
background-color: #e9ecef; background-color: #e9ecef;
} }
// 通知中心样式 // 通知中心样式
.notification-item { .notification-item {
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
&:hover { &:hover {
background-color: $gray-100; background-color: $gray-100;
} }
@ -230,48 +249,48 @@
} }
.dark-pagination .page-link { .dark-pagination .page-link {
color: #000; /* 默认文字颜色 */ color: #000; /* 默认文字颜色 */
background-color: #fff; /* 默认背景 */ background-color: #fff; /* 默认背景 */
border: 1px solid #dee2e6; /* 边框颜色 */ border: 1px solid #dee2e6; /* 边框颜色 */
transition: all 0.3s ease; /* 平滑过渡效果 */ transition: all 0.3s ease; /* 平滑过渡效果 */
} }
/* 激活状态 */ /* 激活状态 */
.dark-pagination .page-item.active .page-link { .dark-pagination .page-item.active .page-link {
background-color: #000 !important; background-color: #000 !important;
border-color: #000; border-color: #000;
color: #fff !important; color: #fff !important;
} }
/* 悬停状态 */ /* 悬停状态 */
.dark-pagination .page-link:hover { .dark-pagination .page-link:hover {
background-color: #f8f9fa; /* 浅灰背景 */ background-color: #f8f9fa; /* 浅灰背景 */
border-color: #adb5bd; border-color: #adb5bd;
} }
/* 禁用状态 */ /* 禁用状态 */
.dark-pagination .page-item.disabled .page-link { .dark-pagination .page-item.disabled .page-link {
color: #6c757d !important; color: #6c757d !important;
background-color: #e9ecef !important; background-color: #e9ecef !important;
border-color: #dee2e6; border-color: #dee2e6;
pointer-events: none; pointer-events: none;
opacity: 0.7; opacity: 0.7;
} }
/* 自定义下拉框 */ /* 自定义下拉框 */
.dark-select { .dark-select {
border: 1px solid #000 !important; border: 1px solid #000 !important;
color: #000 !important; color: #000 !important;
} }
.dark-select:focus { .dark-select:focus {
box-shadow: 0 0 0 0.25rem rgba(0, 0, 0, 0.25); /* 黑色聚焦阴影 */ box-shadow: 0 0 0 0.25rem rgba(0, 0, 0, 0.25); /* 黑色聚焦阴影 */
} }
/* 下拉箭头颜色 */ /* 下拉箭头颜色 */
.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 Styles */
.code-block-container { .code-block-container {
@ -310,7 +329,7 @@
gap: 0.25rem; gap: 0.25rem;
border-radius: 0.25rem; border-radius: 0.25rem;
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
@ -333,11 +352,11 @@
/* Markdown fallback styling */ /* Markdown fallback styling */
.markdown-fallback { .markdown-fallback {
font-size: 0.95rem; font-size: 0.95rem;
.text-danger { .text-danger {
font-weight: 500; font-weight: 500;
} }
pre { pre {
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
@ -350,39 +369,77 @@
/* Streaming message indicator */ /* Streaming message indicator */
.streaming-indicator { .streaming-indicator {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
margin-left: 5px; margin-left: 5px;
.dot { .dot {
width: 6px; width: 6px;
height: 6px; height: 6px;
background-color: #6c757d; background-color: #6c757d;
border-radius: 50%; border-radius: 50%;
margin: 0 2px; margin: 0 2px;
animation: pulse 1.5s infinite ease-in-out; animation: pulse 1.5s infinite ease-in-out;
&.dot1 { &.dot1 {
animation-delay: 0s; animation-delay: 0s;
}
&.dot2 {
animation-delay: 0.3s;
}
&.dot3 {
animation-delay: 0.6s;
}
} }
&.dot2 { @keyframes pulse {
animation-delay: 0.3s; 0%,
100% {
transform: scale(0.8);
opacity: 0.5;
}
50% {
transform: scale(1.2);
opacity: 1;
}
} }
}
&.dot3 {
animation-delay: 0.6s; // SearchBar component styles
.search-input-group {
border: 1px solid #ced4da;
overflow: hidden;
&.rounded-pill {
border-radius: 50rem;
} }
}
.search-input {
@keyframes pulse { border: none;
0%, 100% { box-shadow: none;
transform: scale(0.8);
opacity: 0.5; &:focus {
box-shadow: none;
}
} }
50% {
transform: scale(1.2); .btn {
opacity: 1; border: none;
background-color: transparent;
color: #6c757d;
&:hover,
&:active,
&:focus {
background-color: transparent;
color: #495057;
}
} }
}
} .search-button {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
}

View File

@ -6,18 +6,20 @@ export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd()); const env = loadEnv(mode, process.cwd());
return { return {
base: '/',
plugins: [react()], plugins: [react()],
build: { build: {
outDir: '../dist', outDir: 'dist',
}, },
server: { server: {
port: env.VITE_PORT, port: env.VITE_PORT,
proxy: { proxy: {
'/api': { '/api': {
target: env.VITE_API_URL || 'http://81.69.223.133:58008', target: env.VITE_API_URL || 'http://81.69.223.133:8008',
changeOrigin: true, changeOrigin: true,
}, },
}, },
historyApiFallback: true,
}, },
}; };
}); });