ChatGPT-Next-Web/app/components/search-chat.tsx

169 lines
5.1 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Path } from '../constant';
import CloseIcon from '../icons/close.svg';
import EyeIcon from '../icons/eye.svg';
import Locale from '../locales';
import { useChatStore } from '../store';
import { IconButton } from './button';
import { ErrorBoundary } from './error';
import styles from './mask.module.scss';
interface Item {
id: number;
name: string;
content: string;
}
export function SearchChatPage() {
const navigate = useNavigate();
const chatStore = useChatStore();
const sessions = chatStore.sessions;
const selectSession = chatStore.selectSession;
const [searchResults, setSearchResults] = useState<Item[]>([]);
const previousValueRef = useRef<string>('');
const searchInputRef = useRef<HTMLInputElement>(null);
const doSearch = useCallback((text: string) => {
const lowerCaseText = text.toLowerCase();
const results: Item[] = [];
sessions.forEach((session, index) => {
const fullTextContents: string[] = [];
session.messages.forEach((message) => {
const content = message.content as string;
if (!content.toLowerCase || content === '')
{ return; }
const lowerCaseContent = content.toLowerCase();
// full text search
let pos = lowerCaseContent.indexOf(lowerCaseText);
while (pos !== -1) {
const start = Math.max(0, pos - 35);
const end = Math.min(content.length, pos + lowerCaseText.length + 35);
fullTextContents.push(content.substring(start, end));
pos = lowerCaseContent.indexOf(
lowerCaseText,
pos + lowerCaseText.length,
);
}
});
if (fullTextContents.length > 0) {
results.push({
id: index,
name: session.topic,
content: fullTextContents.join('... '), // concat content with...
});
}
});
// sort by length of matching content
results.sort((a, b) => b.content.length - a.content.length);
return results;
}, []);
useEffect(() => {
const intervalId = setInterval(() => {
if (searchInputRef.current) {
const currentValue = searchInputRef.current.value;
if (currentValue !== previousValueRef.current) {
if (currentValue.length > 0) {
const result = doSearch(currentValue);
setSearchResults(result);
}
previousValueRef.current = currentValue;
}
}
}, 1000);
// Cleanup the interval on component unmount
return () => clearInterval(intervalId);
}, [doSearch]);
return (
<ErrorBoundary>
<div className={styles['mask-page']}>
{/* header */}
<div className="window-header">
<div className="window-header-title">
<div className="window-header-main-title">
{Locale.SearchChat.Page.Title}
</div>
<div className="window-header-submai-title">
{Locale.SearchChat.Page.SubTitle(searchResults.length)}
</div>
</div>
<div className="window-actions">
<div className="window-action-button">
<IconButton
icon={<CloseIcon />}
bordered
onClick={() => navigate(-1)}
/>
</div>
</div>
</div>
<div className={styles['mask-page-body']}>
<div className={styles['mask-filter']}>
{/** 搜索输入框 */}
<input
type="text"
className={styles['search-bar']}
placeholder={Locale.SearchChat.Page.Search}
autoFocus
ref={searchInputRef}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
const searchText = e.currentTarget.value;
if (searchText.length > 0) {
const result = doSearch(searchText);
setSearchResults(result);
}
}
}}
/>
</div>
<div>
{searchResults.map(item => (
<div
className={styles['mask-item']}
key={item.id}
onClick={() => {
navigate(Path.Chat);
selectSession(item.id);
}}
style={{ cursor: 'pointer' }}
>
{/** 搜索匹配的文本 */}
<div className={styles['mask-header']}>
<div className={styles['mask-title']}>
<div className={styles['mask-name']}>{item.name}</div>
{item.content.slice(0, 70)}
</div>
</div>
{/** 操作按钮 */}
<div className={styles['mask-actions']}>
<IconButton
icon={<EyeIcon />}
text={Locale.SearchChat.Item.View}
/>
</div>
</div>
))}
</div>
</div>
</div>
</ErrorBoundary>
);
}