Merge pull request #5274 from Movelocity/feat/search-history

feat: add a page to search chat history
This commit is contained in:
Lloyd Zhou 2024-08-21 00:58:01 +08:00 committed by GitHub
commit a6b14c7910
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 471 additions and 1 deletions

View File

@ -59,6 +59,13 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
loading: () => <Loading noLogo />,
});
const SearchChat = dynamic(
async () => (await import("./search-chat")).SearchChatPage,
{
loading: () => <Loading noLogo />,
},
);
const Sd = dynamic(async () => (await import("./sd")).Sd, {
loading: () => <Loading noLogo />,
});
@ -174,6 +181,7 @@ function Screen() {
<Route path={Path.Home} element={<Chat />} />
<Route path={Path.NewChat} element={<NewChat />} />
<Route path={Path.Masks} element={<MaskPage />} />
<Route path={Path.SearchChat} element={<SearchChat />} />
<Route path={Path.Chat} element={<Chat />} />
<Route path={Path.Settings} element={<Settings />} />
</Routes>

View File

@ -0,0 +1,167 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { ErrorBoundary } from "./error";
import styles from "./mask.module.scss";
import { useNavigate } from "react-router-dom";
import { IconButton } from "./button";
import CloseIcon from "../icons/close.svg";
import EyeIcon from "../icons/eye.svg";
import Locale from "../locales";
import { Path } from "../constant";
import { useChatStore } from "../store";
type 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>
);
}

View File

@ -1,3 +1,5 @@
import path from "path";
export const OWNER = "ChatGPTNextWeb";
export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
@ -41,6 +43,7 @@ export enum Path {
Sd = "/sd",
SdNew = "/sd-new",
Artifacts = "/artifacts",
SearchChat = "/search-chat",
}
export enum ApiPath {
@ -475,4 +478,7 @@ export const internalAllowedWebDavEndpoints = [
];
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
export const PLUGINS = [{ name: "Stable Diffusion", path: Path.Sd }];
export const PLUGINS = [
{ name: "Stable Diffusion", path: Path.Sd },
{ name: "Search Chat", path: Path.SearchChat },
];

1
app/icons/zoom.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" height="1.2rem" viewBox="0 0 24 24"><g fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></g></svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@ -459,6 +459,21 @@ const ar: PartialLocaleType = {
FineTuned: {
Sysmessage: "أنت مساعد",
},
SearchChat: {
Name: "بحث",
Page: {
Title: "البحث في سجلات الدردشة",
Search: "أدخل كلمات البحث",
NoResult: "لم يتم العثور على نتائج",
NoData: "لا توجد بيانات",
Loading: "جارٍ التحميل",
SubTitle: (count: number) => `تم العثور على ${count} نتائج`,
},
Item: {
View: "عرض",
},
},
Mask: {
Name: "القناع",
Page: {

View File

@ -466,6 +466,21 @@ const bn: PartialLocaleType = {
FineTuned: {
Sysmessage: "আপনি একজন সহকারী",
},
SearchChat: {
Name: "অনুসন্ধান",
Page: {
Title: "চ্যাট রেকর্ড অনুসন্ধান করুন",
Search: "অনুসন্ধান কীওয়ার্ড লিখুন",
NoResult: "কোন ফলাফল পাওয়া যায়নি",
NoData: "কোন তথ্য নেই",
Loading: "লোড হচ্ছে",
SubTitle: (count: number) => `${count} টি ফলাফল পাওয়া গেছে`,
},
Item: {
View: "দেখুন",
},
},
Mask: {
Name: "মাস্ক",
Page: {

View File

@ -519,6 +519,21 @@ const cn = {
FineTuned: {
Sysmessage: "你是一个助手",
},
SearchChat: {
Name: "搜索",
Page: {
Title: "搜索聊天记录",
Search: "输入搜索关键词",
NoResult: "没有找到结果",
NoData: "没有数据",
Loading: "加载中",
SubTitle: (count: number) => `搜索到 ${count} 条结果`,
},
Item: {
View: "查看",
},
},
Mask: {
Name: "面具",
Page: {

View File

@ -467,6 +467,21 @@ const cs: PartialLocaleType = {
FineTuned: {
Sysmessage: "Jste asistent",
},
SearchChat: {
Name: "Hledat",
Page: {
Title: "Hledat v historii chatu",
Search: "Zadejte hledané klíčové slovo",
NoResult: "Nebyly nalezeny žádné výsledky",
NoData: "Žádná data",
Loading: "Načítání",
SubTitle: (count: number) => `Nalezeno ${count} výsledků`,
},
Item: {
View: "Zobrazit",
},
},
Mask: {
Name: "Maska",
Page: {

View File

@ -482,6 +482,21 @@ const de: PartialLocaleType = {
FineTuned: {
Sysmessage: "Du bist ein Assistent",
},
SearchChat: {
Name: "Suche",
Page: {
Title: "Chatverlauf durchsuchen",
Search: "Suchbegriff eingeben",
NoResult: "Keine Ergebnisse gefunden",
NoData: "Keine Daten",
Loading: "Laden",
SubTitle: (count: number) => `${count} Ergebnisse gefunden`,
},
Item: {
View: "Ansehen",
},
},
Mask: {
Name: "Masken",
Page: {

View File

@ -527,6 +527,21 @@ const en: LocaleType = {
FineTuned: {
Sysmessage: "You are an assistant that",
},
SearchChat: {
Name: "Search",
Page: {
Title: "Search Chat History",
Search: "Enter search query to search chat history",
NoResult: "No results found",
NoData: "No data",
Loading: "Loading...",
SubTitle: (count: number) => `Found ${count} results`,
},
Item: {
View: "View",
},
},
Mask: {
Name: "Mask",
Page: {

View File

@ -480,6 +480,21 @@ const es: PartialLocaleType = {
FineTuned: {
Sysmessage: "Eres un asistente",
},
SearchChat: {
Name: "Buscar",
Page: {
Title: "Buscar en el historial de chat",
Search: "Ingrese la palabra clave de búsqueda",
NoResult: "No se encontraron resultados",
NoData: "Sin datos",
Loading: "Cargando",
SubTitle: (count: number) => `Se encontraron ${count} resultados`,
},
Item: {
View: "Ver",
},
},
Mask: {
Name: "Máscara",
Page: {

View File

@ -480,6 +480,21 @@ const fr: PartialLocaleType = {
FineTuned: {
Sysmessage: "Vous êtes un assistant",
},
SearchChat: {
Name: "Recherche",
Page: {
Title: "Rechercher dans l'historique des discussions",
Search: "Entrez le mot-clé de recherche",
NoResult: "Aucun résultat trouvé",
NoData: "Aucune donnée",
Loading: "Chargement",
SubTitle: (count: number) => `${count} résultats trouvés`,
},
Item: {
View: "Voir",
},
},
Mask: {
Name: "Masque",
Page: {

View File

@ -470,6 +470,21 @@ const id: PartialLocaleType = {
FineTuned: {
Sysmessage: "Anda adalah seorang asisten",
},
SearchChat: {
Name: "Cari",
Page: {
Title: "Cari riwayat obrolan",
Search: "Masukkan kata kunci pencarian",
NoResult: "Tidak ada hasil ditemukan",
NoData: "Tidak ada data",
Loading: "Memuat",
SubTitle: (count: number) => `Ditemukan ${count} hasil`,
},
Item: {
View: "Lihat",
},
},
Mask: {
Name: "Masker",
Page: {

View File

@ -481,6 +481,21 @@ const it: PartialLocaleType = {
FineTuned: {
Sysmessage: "Sei un assistente",
},
SearchChat: {
Name: "Cerca",
Page: {
Title: "Cerca nei messaggi",
Search: "Inserisci parole chiave per la ricerca",
NoResult: "Nessun risultato trovato",
NoData: "Nessun dato",
Loading: "Caricamento in corso",
SubTitle: (count: number) => `Trovati ${count} risultati`,
},
Item: {
View: "Visualizza",
},
},
Mask: {
Name: "Maschera",
Page: {

View File

@ -460,9 +460,27 @@ const jp: PartialLocaleType = {
Plugin: {
Name: "プラグイン",
},
Discovery: {
Name: "発見",
},
FineTuned: {
Sysmessage: "あなたはアシスタントです",
},
SearchChat: {
Name: "検索",
Page: {
Title: "チャット履歴を検索",
Search: "検索キーワードを入力",
NoResult: "結果が見つかりませんでした",
NoData: "データがありません",
Loading: "読み込み中",
SubTitle: (count: number) => `${count} 件の結果が見つかりました`,
},
Item: {
View: "表示",
},
},
Mask: {
Name: "マスク",
Page: {

View File

@ -458,6 +458,21 @@ const ko: PartialLocaleType = {
FineTuned: {
Sysmessage: "당신은 보조자입니다.",
},
SearchChat: {
Name: "검색",
Page: {
Title: "채팅 기록 검색",
Search: "검색어 입력",
NoResult: "결과를 찾을 수 없습니다",
NoData: "데이터가 없습니다",
Loading: "로딩 중",
SubTitle: (count: number) => `${count}개의 결과를 찾았습니다`,
},
Item: {
View: "보기",
},
},
Mask: {
Name: "마스크",
Page: {

View File

@ -474,6 +474,21 @@ const no: PartialLocaleType = {
FineTuned: {
Sysmessage: "Du er en assistent",
},
SearchChat: {
Name: "Søk",
Page: {
Title: "Søk i chatthistorikk",
Search: "Skriv inn søkeord",
NoResult: "Ingen resultater funnet",
NoData: "Ingen data",
Loading: "Laster inn",
SubTitle: (count: number) => `Fant ${count} resultater`,
},
Item: {
View: "Vis",
},
},
Mask: {
Name: "Maske",
Page: {

View File

@ -405,6 +405,21 @@ const pt: PartialLocaleType = {
FineTuned: {
Sysmessage: "Você é um assistente que",
},
SearchChat: {
Name: "Pesquisar",
Page: {
Title: "Pesquisar histórico de chat",
Search: "Digite palavras-chave para pesquisa",
NoResult: "Nenhum resultado encontrado",
NoData: "Sem dados",
Loading: "Carregando",
SubTitle: (count: number) => `Encontrado ${count} resultados`,
},
Item: {
View: "Ver",
},
},
Mask: {
Name: "Máscara",
Page: {

View File

@ -471,6 +471,21 @@ const ru: PartialLocaleType = {
FineTuned: {
Sysmessage: "Вы - помощник",
},
SearchChat: {
Name: "Поиск",
Page: {
Title: "Поиск в истории чатов",
Search: "Введите ключевые слова для поиска",
NoResult: "Результатов не найдено",
NoData: "Нет данных",
Loading: "Загрузка",
SubTitle: (count: number) => `Найдено ${count} результатов`,
},
Item: {
View: "Просмотр",
},
},
Mask: {
Name: "Маска",
Page: {

View File

@ -423,6 +423,21 @@ const sk: PartialLocaleType = {
FineTuned: {
Sysmessage: "Ste asistent, ktorý",
},
SearchChat: {
Name: "Hľadať",
Page: {
Title: "Hľadať v histórii chatu",
Search: "Zadajte kľúčové slová na vyhľadávanie",
NoResult: "Nenašli sa žiadne výsledky",
NoData: "Žiadne údaje",
Loading: "Načítava sa",
SubTitle: (count: number) => `Nájdených ${count} výsledkov`,
},
Item: {
View: "Zobraziť",
},
},
Mask: {
Name: "Maska",
Page: {

View File

@ -470,6 +470,21 @@ const tr: PartialLocaleType = {
FineTuned: {
Sysmessage: "Sen bir asistansın",
},
SearchChat: {
Name: "Ara",
Page: {
Title: "Sohbet geçmişini ara",
Search: "Arama anahtar kelimelerini girin",
NoResult: "Sonuç bulunamadı",
NoData: "Veri yok",
Loading: "Yükleniyor",
SubTitle: (count: number) => `${count} sonuç bulundu`,
},
Item: {
View: "Görüntüle",
},
},
Mask: {
Name: "Maske",
Page: {

View File

@ -452,6 +452,21 @@ const tw = {
},
},
},
SearchChat: {
Name: "搜索",
Page: {
Title: "搜索聊天記錄",
Search: "輸入搜索關鍵詞",
NoResult: "沒有找到結果",
NoData: "沒有數據",
Loading: "加載中",
SubTitle: (count: number) => `找到 ${count} 條結果`,
},
Item: {
View: "查看",
},
},
NewChat: {
Return: "返回",
Skip: "跳過",

View File

@ -466,6 +466,21 @@ const vi: PartialLocaleType = {
FineTuned: {
Sysmessage: "Bạn là một trợ lý",
},
SearchChat: {
Name: "Tìm kiếm",
Page: {
Title: "Tìm kiếm lịch sử trò chuyện",
Search: "Nhập từ khóa tìm kiếm",
NoResult: "Không tìm thấy kết quả",
NoData: "Không có dữ liệu",
Loading: "Đang tải",
SubTitle: (count: number) => `Tìm thấy ${count} kết quả`,
},
Item: {
View: "Xem",
},
},
Mask: {
Name: "Mặt nạ",
Page: {