Просмотр исходного кода

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

feat: add a page to search chat history
Lloyd Zhou 1 год назад
Родитель
Сommit
a6b14c7910

+ 8 - 0
app/components/home.tsx

@@ -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>

+ 167 - 0
app/components/search-chat.tsx

@@ -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>
+  );
+}

+ 7 - 1
app/constant.ts

@@ -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 - 0
app/icons/zoom.svg

@@ -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>

+ 15 - 0
app/locales/ar.ts

@@ -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: {

+ 15 - 0
app/locales/bn.ts

@@ -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: {

+ 15 - 0
app/locales/cn.ts

@@ -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: {

+ 15 - 0
app/locales/cs.ts

@@ -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: {

+ 15 - 0
app/locales/de.ts

@@ -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: {

+ 15 - 0
app/locales/en.ts

@@ -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: {

+ 15 - 0
app/locales/es.ts

@@ -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: {

+ 15 - 0
app/locales/fr.ts

@@ -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: {

+ 15 - 0
app/locales/id.ts

@@ -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: {

+ 15 - 0
app/locales/it.ts

@@ -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: {

+ 18 - 0
app/locales/jp.ts

@@ -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: {

+ 15 - 0
app/locales/ko.ts

@@ -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: {

+ 15 - 0
app/locales/no.ts

@@ -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: {

+ 15 - 0
app/locales/pt.ts

@@ -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: {

+ 15 - 0
app/locales/ru.ts

@@ -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: {

+ 15 - 0
app/locales/sk.ts

@@ -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: {

+ 15 - 0
app/locales/tr.ts

@@ -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: {

+ 15 - 0
app/locales/tw.ts

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

+ 15 - 0
app/locales/vi.ts

@@ -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: {