[dev]update search

This commit is contained in:
susie-laptop 2025-03-22 22:53:37 -04:00
parent 167b06315d
commit fbfff98123
12 changed files with 144 additions and 184 deletions

View File

@ -47,32 +47,23 @@ const SearchBar = ({
};
}, []);
//
const handleFocus = () => {
if (searchKeyword.trim().length > 0) {
//
useEffect(() => {
if (isSearching && searchResults.length > 0) {
setShowDropdown(true);
}
};
}, [isSearching, searchResults]);
//
const handleInputChange = (e) => {
const value = e.target.value;
onSearchChange(e);
if (value.trim().length > 0) {
setShowDropdown(true);
} else {
setShowDropdown(false);
}
};
//
const handleSubmit = (e) => {
e.preventDefault();
onSearch(e);
if (searchKeyword.trim().length > 0) {
setShowDropdown(true);
}
// (searchResults)
};
return (
@ -86,7 +77,6 @@ const SearchBar = ({
placeholder={placeholder}
value={searchKeyword}
onChange={handleInputChange}
onFocus={handleFocus}
/>
{searchKeyword.trim() && (
<button
@ -107,9 +97,9 @@ const SearchBar = ({
</div>
</form>
{/* 搜索结果下拉框 */}
{showDropdown && (
<div className='position-absolute bg-white shadow-sm rounded-3 mt-1 w-100 search-results-dropdown'>
{/* 搜索结果下拉框 - 仅在用户搜索且有结果时显示 */}
{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='p-2 overflow-auto' style={{ maxHeight: '350px', zIndex: '1050' }}>
{isSearchLoading ? (
<div className='text-center p-3'>

View File

@ -11,7 +11,7 @@ const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => {
}, duration);
return () => clearTimeout(timer);
}
}, [duration, onClose]);
}, [message, duration, onClose]);
const icons = {
success: 'check-circle-fill',
@ -20,22 +20,21 @@ const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => {
danger: 'exclamation-triangle-fill',
};
//
const handleClose = (e) => {
e.preventDefault();
if (onClose) onClose();
};
return (
<>
<div
className={`snackbar alert alert-${type} d-flex align-items-center justify-content-between position-fixed top-10 start-50 translate-middle w-50 alert-dismissible z-2 gap-2`}
className={`snackbar alert alert-${type} d-flex align-items-center justify-content-between position-fixed top-10 start-50 translate-middle w-50 z-2 gap-2`}
role='alert'
>
<SvgIcon className={icons[type]} />
<div className='flex-fill'>{message}</div>
<button
type='button'
className='btn-close flex-end'
data-bs-dismiss='alert'
aria-label='Close'
></button>
<button type='button' className='btn-close flex-end' onClick={handleClose} aria-label='Close'></button>
</div>
</>
);
};

View File

@ -7,7 +7,6 @@ export default function Mainlayout({ children }) {
return (
<>
<HeaderWithNav />
<NotificationSnackbar />
{children}
</>
);

View File

@ -18,10 +18,11 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
error: messagesError,
} = useSelector((state) => state.chat.messages);
const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage);
const knowledgeBase = useSelector((state) =>
state.knowledgeBase.list.data?.items?.find((kb) => kb.id === knowledgeBaseId)
);
const isLoadingKnowledgeBases = useSelector((state) => state.knowledgeBase.list.isLoading);
// 使Redux
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []);
const knowledgeBase = knowledgeBases.find((kb) => kb.id === knowledgeBaseId);
const isLoadingKnowledgeBases = useSelector((state) => state.knowledgeBase.loading);
//
useEffect(() => {
@ -56,7 +57,7 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
// Redux store
useEffect(() => {
if (!knowledgeBase && !isLoadingKnowledgeBases) {
dispatch(fetchKnowledgeBases());
dispatch(fetchKnowledgeBases({ page: 1, page_size: 50 }));
}
}, [dispatch, knowledgeBase, isLoadingKnowledgeBases]);

View File

@ -8,23 +8,20 @@ import SvgIcon from '../../components/SvgIcon';
export default function NewChat() {
const navigate = useNavigate();
const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
// Redux store
const { data, status, error } = useSelector((state) => state.knowledgeBase.list);
const knowledgeBases = data?.items || [];
const isLoading = status === 'loading';
// Redux store - 使
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []);
const isLoading = useSelector((state) => state.knowledgeBase.loading);
const error = useSelector((state) => state.knowledgeBase.error);
//
useEffect(() => {
if (!data?.items?.length && status !== 'loading') {
dispatch(fetchKnowledgeBases());
}
}, [dispatch, data, status]);
dispatch(fetchKnowledgeBases({ page: 1, page_size: 50 }));
}, [dispatch]);
//
useEffect(() => {
if (status === 'failed' && error) {
if (error) {
dispatch(
showNotification({
message: `获取知识库列表失败: ${error.message || error}`,
@ -32,7 +29,7 @@ export default function NewChat() {
})
);
}
}, [status, error, dispatch]);
}, [error, dispatch]);
// can_read
const readableKnowledgeBases = knowledgeBases.filter((kb) => kb.permissions && kb.permissions.can_read === true);

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { showNotification } from '../../../store/notification.slice';
import { getKnowledgeBaseById } from '../../../store/knowledgeBase/knowledgeBase.thunks';
import { fetchKnowledgeBaseDetail } from '../../../store/knowledgeBase/knowledgeBase.thunks';
import SvgIcon from '../../../components/SvgIcon';
import DatasetTab from './DatasetTab';
import SettingsTab from './SettingsTab';
@ -13,14 +13,15 @@ export default function KnowledgeBaseDetail() {
const dispatch = useDispatch();
const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets');
// Get knowledge base details from Redux store
const { data: knowledgeBase, status, error } = useSelector((state) => state.knowledgeBase.detail);
const isLoading = status === 'loading';
// Get knowledge base details from Redux store - 使
const knowledgeBase = useSelector((state) => state.knowledgeBase.currentKnowledgeBase);
const loading = useSelector((state) => state.knowledgeBase.loading);
const error = useSelector((state) => state.knowledgeBase.error);
// Fetch knowledge base details when component mounts or ID changes
useEffect(() => {
if (id) {
dispatch(getKnowledgeBaseById(id));
dispatch(fetchKnowledgeBaseDetail(id));
}
}, [dispatch, id]);
@ -33,7 +34,7 @@ export default function KnowledgeBaseDetail() {
// If knowledge base not found, show notification and redirect
useEffect(() => {
if (status === 'failed' && error) {
if (!loading && error) {
dispatch(
showNotification({
message: `获取知识库失败: ${error.message || '未找到知识库'}`,
@ -42,7 +43,7 @@ export default function KnowledgeBaseDetail() {
);
navigate('/knowledge-base');
}
}, [status, error, dispatch, navigate]);
}, [loading, error, dispatch, navigate]);
// Handle tab change
const handleTabChange = (tab) => {
@ -51,7 +52,7 @@ export default function KnowledgeBaseDetail() {
};
// Show loading state if knowledge base not loaded yet
if (isLoading || !knowledgeBase) {
if (loading || !knowledgeBase) {
return (
<div className='container-fluid px-4 py-5 text-center'>
<div className='spinner-border' role='status'>

View File

@ -16,7 +16,6 @@ import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal'
import Pagination from '../../components/Pagination';
import SearchBar from '../../components/SearchBar';
import ApiModeSwitch from '../../components/ApiModeSwitch';
import debounce from 'lodash/debounce';
//
import KnowledgeBaseList from './components/KnowledgeBaseList';
@ -47,7 +46,7 @@ export default function KnowledgeBase() {
// Search state
const [searchKeyword, setSearchKeyword] = useState('');
const [isSearching, setIsSearching] = useState(false);
const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false);
// Pagination state
const [pagination, setPagination] = useState({
@ -68,100 +67,26 @@ export default function KnowledgeBase() {
const searchResults = useSelector((state) => state.knowledgeBase.searchResults);
const searchLoading = useSelector((state) => state.knowledgeBase.searchLoading);
// Determine which data to display based on search state
const displayData = isSearching ? searchResults : knowledgeBases;
const displayTotal = paginationData.total;
const displayStatus = loading ? 'loading' : 'succeeded';
const displayError = error;
// Fetch knowledge bases when component mounts or pagination changes
useEffect(() => {
if (!isSearching) {
//
dispatch(fetchKnowledgeBases(pagination));
} else if (searchKeyword.trim()) {
dispatch(
searchKnowledgeBases({
keyword: searchKeyword,
page: pagination.page,
page_size: pagination.page_size,
})
);
}
}, [dispatch, pagination.page, pagination.page_size, isSearching, searchKeyword]);
//
const debouncedSearch = useCallback(
debounce((keyword) => {
if (keyword.trim()) {
dispatch(
searchKnowledgeBases({
keyword,
page: 1,
page_size: 5,
})
);
} else {
dispatch(clearSearchResults());
}
}, 300),
[dispatch]
);
// Handle search input change
const handleSearchInputChange = (e) => {
const value = e.target.value;
setSearchKeyword(value);
//
if (value.trim()) {
debouncedSearch(value);
} else {
dispatch(clearSearchResults());
}
};
// Handle search submit
const handleSearch = (e) => {
e.preventDefault();
if (searchKeyword.trim()) {
setIsSearching(true);
setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page
dispatch(
searchKnowledgeBases({
keyword: searchKeyword,
page: 1,
page_size: pagination.page_size,
})
);
} else {
// If search is empty, reset to normal list view
handleClearSearch();
}
};
// Handle clear search
const handleClearSearch = () => {
setSearchKeyword('');
setIsSearching(false);
setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page
dispatch(clearSearchResults());
};
}, [dispatch, pagination.page, pagination.page_size]);
// Show loading state while fetching data
const isLoading = displayStatus === 'loading';
const isLoading = loading;
// Show error notification if fetch fails
useEffect(() => {
if (displayStatus === 'failed' && displayError) {
if (!isLoading && error) {
dispatch(
showNotification({
message: `获取知识库列表失败: ${displayError.message || displayError}`,
message: `获取知识库列表失败: ${error.message || error}`,
type: 'danger',
})
);
}
}, [displayStatus, displayError, dispatch]);
}, [isLoading, error, dispatch]);
// Show notification for operation status
useEffect(() => {
@ -169,17 +94,7 @@ export default function KnowledgeBase() {
//
// Refresh the list after successful operation
if (isSearching && searchKeyword.trim()) {
dispatch(
searchKnowledgeBases({
keyword: searchKeyword,
page: pagination.page,
page_size: pagination.page_size,
})
);
} else {
dispatch(fetchKnowledgeBases(pagination));
}
} else if (operationStatus === 'failed' && operationError) {
dispatch(
showNotification({
@ -188,7 +103,47 @@ export default function KnowledgeBase() {
})
);
}
}, [operationStatus, operationError, dispatch, pagination, isSearching, searchKeyword]);
}, [operationStatus, operationError, dispatch, pagination]);
// Handle search input change
const handleSearchInputChange = (e) => {
const value = e.target.value;
setSearchKeyword(value);
//
if (!value.trim()) {
dispatch(clearSearchResults());
setIsSearchDropdownOpen(false);
}
};
// Handle search submit -
const handleSearch = (e) => {
e.preventDefault();
if (searchKeyword.trim()) {
// isSearching
dispatch(
searchKnowledgeBases({
keyword: searchKeyword,
page: 1,
page_size: 5, //
})
);
setIsSearchDropdownOpen(true);
} else {
//
handleClearSearch();
}
};
// Handle clear search
const handleClearSearch = () => {
setSearchKeyword('');
//
setIsSearchDropdownOpen(false);
dispatch(clearSearchResults());
};
// Handle pagination change
const handlePageChange = (newPage) => {
@ -426,7 +381,7 @@ export default function KnowledgeBase() {
};
// Calculate total pages
const totalPages = Math.ceil(displayTotal / pagination.page_size);
const totalPages = Math.ceil(paginationData.total / pagination.page_size);
//
const handleOpenCreateModal = () => {
@ -458,7 +413,7 @@ export default function KnowledgeBase() {
<div className='d-flex justify-content-between align-items-center mb-3'>
<SearchBar
searchKeyword={searchKeyword}
isSearching={isSearching}
isSearching={isSearchDropdownOpen} //
onSearchChange={handleSearchInputChange}
onSearch={handleSearch}
onClearSearch={handleClearSearch}
@ -474,12 +429,6 @@ export default function KnowledgeBase() {
</button>
</div>
{isSearching && (
<div className='alert alert-info'>
搜索结果: "{searchKeyword}" - 找到 {displayTotal} 个知识库
</div>
)}
{isLoading ? (
<div className='d-flex justify-content-center my-5'>
<div className='spinner-border' role='status'>
@ -489,14 +438,14 @@ export default function KnowledgeBase() {
) : (
<>
<KnowledgeBaseList
knowledgeBases={displayData}
isSearching={isSearching}
knowledgeBases={knowledgeBases}
isSearching={false} // false
onCardClick={handleCardClick}
onRequestAccess={handleRequestAccess}
onDelete={handleDelete}
/>
{/* Pagination */}
{/* Pagination - 始终显示 */}
{totalPages > 1 && (
<Pagination
currentPage={pagination.page}

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
import { checkAuthThunk, loginThunk } from '../../store/auth/auth.thunk';
import { showNotification } from '../../store/notification.slice';
export default function Login() {
const dispatch = useDispatch();
@ -10,6 +11,7 @@ export default function Login() {
const [password, setPassword] = useState('leader123');
const [errors, setErrors] = useState({});
const [submitted, setSubmitted] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { user } = useSelector((state) => state.auth);
@ -22,18 +24,20 @@ export default function Login() {
try {
await dispatch(checkAuthThunk()).unwrap();
if (user) navigate('/');
} catch (error) {}
} catch (error) {
//
}
};
const validateForm = () => {
const newErrors = {};
if (!username) {
newErrors.username = 'Username is required';
newErrors.username = '请输入用户名';
}
if (!password) {
newErrors.password = 'Password is required';
newErrors.password = '请输入密码';
} else if (password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
newErrors.password = '密码长度不能少于6个字符';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
@ -44,17 +48,19 @@ export default function Login() {
setSubmitted(true);
if (validateForm()) {
console.log('Form submitted successfully!');
console.log('Username:', username);
console.log('Password:', password);
setIsLoading(true);
try {
await dispatch(loginThunk({ username, password })).unwrap();
navigate('/');
} catch (error) {
// thunk
console.error('Login failed:', error);
} finally {
setIsLoading(false);
}
}
};
return (
<div className='position-absolute top-50 start-50 translate-middle d-flex flex-column gap-4 align-items-center'>
<div className='title text-center h1'>OOIN 智能知识库</div>
@ -69,7 +75,7 @@ export default function Login() {
type='text'
className={`form-control form-control-lg${submitted && errors.username ? ' is-invalid' : ''}`}
id='username'
placeholder='Username'
placeholder='用户名'
required
onChange={(e) => setUsername(e.target.value.trim())}
></input>
@ -80,7 +86,7 @@ export default function Login() {
value={password}
type='password'
id='password'
placeholder='Password'
placeholder='密码'
required
className={`form-control form-control-lg${submitted && errors.password ? ' is-invalid' : ''}`}
aria-describedby='passwordHelpBlock'
@ -89,14 +95,25 @@ export default function Login() {
{submitted && errors.password && <div className='invalid-feedback'>{errors.password}</div>}
</div>
<Link to='#' className='find-password text-body-secondary'>
Forgot password?
忘记密码?
</Link>
<button type='submit' className='btn btn-dark btn-lg w-100'>
Login
<button type='submit' className='btn btn-dark btn-lg w-100' disabled={isLoading}>
{isLoading ? (
<>
<span
className='spinner-border spinner-border-sm me-2'
role='status'
aria-hidden='true'
></span>
登录中...
</>
) : (
'登录'
)}
</button>
</form>
<Link to='/signup' className='go-to-signup w-100 link-underline-light h5 text-center'>
Need Account?
没有账号去注册
</Link>
</div>
);

View File

@ -10,6 +10,7 @@ import Login from '../pages/Auth/Login';
import Signup from '../pages/Auth/Signup';
import ProtectedRoute from './protectedRoute';
import { useSelector } from 'react-redux';
import NotificationSnackbar from '../components/NotificationSnackbar';
function AppRouter() {
const { user } = useSelector((state) => state.auth);
@ -19,6 +20,8 @@ function AppRouter() {
return (
<Suspense fallback={<Loading />}>
<NotificationSnackbar />
<Routes>
<Route element={<ProtectedRoute />}>
<Route

View File

@ -101,7 +101,7 @@ const get = async (url, params = {}) => {
return await mockGet(url, params);
}
const res = await api.get(url, { params });
const res = await api.get(url, { ...params });
return res.data;
} catch (error) {
if (!hasCheckedServer || (error.request && !error.response)) {

View File

@ -10,9 +10,12 @@ export const loginThunk = createAsyncThunk(
'auth/login',
async ({ username, password }, { rejectWithValue, dispatch }) => {
try {
const { message, data } = await post('/auth/login/', { username, password });
console.log('data', data);
const { message, data, code } = await post('/auth/login/', { username, password });
console.log('code', code);
if (code !== 200) {
throw new Error(message || 'Something went wrong');
}
if (!data) {
throw new Error(message || 'Something went wrong');
}
@ -24,6 +27,7 @@ export const loginThunk = createAsyncThunk(
return data;
} catch (error) {
const errorMessage = error.response?.data?.message || 'Something went wrong';
console.log(errorMessage);
dispatch(
showNotification({
message: errorMessage,

View File

@ -34,7 +34,7 @@ export const fetchKnowledgeBases = createAsyncThunk(
export const searchKnowledgeBases = createAsyncThunk('knowledgeBase/search', async (params, { rejectWithValue }) => {
try {
const { keyword, page = 1, page_size = 10 } = params;
const response = await get('/knowledge-bases/search/', {
const response = await get('/knowledge-bases/search', {
params: {
keyword,
page,