victor HF Staff commited on
Commit
af88f33
·
1 Parent(s): 3085f3e

chore: remove search chat feature (UI and /conversations/search API)

Browse files
src/lib/components/NavMenu.svelte CHANGED
@@ -23,12 +23,7 @@
23
  import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
24
  import { goto } from "$app/navigation";
25
  import { browser } from "$app/environment";
26
- import { toggleSearch } from "./chat/Search.svelte";
27
- import CarbonSearch from "~icons/carbon/search";
28
- import { closeMobileNav } from "./MobileNav.svelte";
29
  import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
30
-
31
- import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
32
  import { useAPIClient, handleResponse } from "$lib/APIClient";
33
 
34
  const publicConfig = usePublicConfig();
@@ -118,24 +113,10 @@
118
  </a>
119
  {/if}
120
  </div>
 
121
  <div
122
  class="scrollbar-custom flex touch-pan-y flex-col gap-1 overflow-y-auto rounded-r-xl from-gray-50 px-3 pb-3 pt-2 text-[.9rem] dark:from-gray-800/30 max-sm:bg-gradient-to-t md:bg-gradient-to-l"
123
  >
124
- <button
125
- class="group mx-auto flex w-full flex-row items-center justify-stretch gap-x-2 rounded-xl px-2 py-1 align-middle text-gray-600 hover:bg-gray-500/20 dark:text-gray-400"
126
- onclick={() => {
127
- closeMobileNav();
128
- toggleSearch();
129
- }}
130
- >
131
- <CarbonSearch class="text-xs" />
132
- <span class="block">Search chats</span>
133
- {#if !isVirtualKeyboard()}
134
- <span class="invisible ml-auto text-xs text-gray-500 group-hover:visible"
135
- ><kbd>ctrl</kbd>+<kbd>k</kbd></span
136
- >
137
- {/if}
138
- </button>
139
  {#await groupedConversations}
140
  {#if $page.data.nConversations > 0}
141
  <div class="overflow-y-hidden">
 
23
  import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
24
  import { goto } from "$app/navigation";
25
  import { browser } from "$app/environment";
 
 
 
26
  import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
 
 
27
  import { useAPIClient, handleResponse } from "$lib/APIClient";
28
 
29
  const publicConfig = usePublicConfig();
 
113
  </a>
114
  {/if}
115
  </div>
116
+
117
  <div
118
  class="scrollbar-custom flex touch-pan-y flex-col gap-1 overflow-y-auto rounded-r-xl from-gray-50 px-3 pb-3 pt-2 text-[.9rem] dark:from-gray-800/30 max-sm:bg-gradient-to-t md:bg-gradient-to-l"
119
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  {#await groupedConversations}
121
  {#if $page.data.nConversations > 0}
122
  <div class="overflow-y-hidden">
src/lib/components/chat/Search.svelte DELETED
@@ -1,201 +0,0 @@
1
- <script lang="ts" module>
2
- export function toggleSearch() {
3
- searchOpen = !searchOpen;
4
- }
5
-
6
- let searchOpen: boolean = $state(false);
7
- </script>
8
-
9
- <script lang="ts">
10
- import { debounce } from "$lib/utils/debounce";
11
- import NavConversationItem from "../NavConversationItem.svelte";
12
- import { titles } from "../NavMenu.svelte";
13
- import { beforeNavigate } from "$app/navigation";
14
-
15
- import CarbonClose from "~icons/carbon/close";
16
- import { fly } from "svelte/transition";
17
- import InfiniteScroll from "../InfiniteScroll.svelte";
18
- import { handleResponse, useAPIClient, type Success } from "$lib/APIClient";
19
-
20
- const client = useAPIClient();
21
-
22
- let searchContainer: HTMLDivElement | undefined = $state(undefined);
23
- let inputElement: HTMLInputElement | undefined = $state(undefined);
24
-
25
- let searchInput: string = $state("");
26
- let debouncedInput: string = $state("");
27
- let hasMore = $state(true);
28
-
29
- let pending: boolean = $state(false);
30
-
31
- let conversations: NonNullable<Success<typeof client.conversations.search.get>> = $state([]);
32
-
33
- let page: number = $state(0);
34
-
35
- const dateRanges = [
36
- new Date().setDate(new Date().getDate() - 1),
37
- new Date().setDate(new Date().getDate() - 7),
38
- new Date().setMonth(new Date().getMonth() - 1),
39
- ];
40
-
41
- let groupedConversations = $derived({
42
- today: conversations.filter(({ updatedAt }) => updatedAt.getTime() > dateRanges[0]),
43
- week: conversations.filter(
44
- ({ updatedAt }) => updatedAt.getTime() > dateRanges[1] && updatedAt.getTime() < dateRanges[0]
45
- ),
46
- month: conversations.filter(
47
- ({ updatedAt }) => updatedAt.getTime() > dateRanges[2] && updatedAt.getTime() < dateRanges[1]
48
- ),
49
- older: conversations.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]),
50
- });
51
-
52
- const update = debounce(async (v: string) => {
53
- if (debouncedInput !== v) {
54
- conversations = [];
55
- page = 0;
56
- hasMore = true;
57
- }
58
- debouncedInput = v;
59
- pending = true;
60
- try {
61
- await handleVisible(v);
62
- } finally {
63
- pending = false;
64
- }
65
- }, 300);
66
-
67
- const handleBackdropClick = (event: MouseEvent) => {
68
- if (!searchOpen || !searchContainer) return;
69
-
70
- const target = event.target;
71
- if (!(target instanceof Node) || !searchContainer.contains(target)) {
72
- searchOpen = false;
73
- }
74
- };
75
-
76
- async function handleVisible(v: string) {
77
- const newConvs = await client.conversations.search
78
- .get({
79
- query: {
80
- q: v,
81
- p: page++,
82
- },
83
- })
84
- .then(handleResponse)
85
- .catch(() => []);
86
-
87
- if (newConvs.length === 0) {
88
- hasMore = false;
89
- }
90
-
91
- conversations = [...conversations, ...newConvs];
92
- }
93
-
94
- $effect(() => update(searchInput));
95
-
96
- function handleKeydown(event: KeyboardEvent) {
97
- if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
98
- if (!searchOpen) {
99
- searchOpen = true;
100
- }
101
- event.preventDefault();
102
- event.stopPropagation();
103
- }
104
-
105
- if (searchOpen && event.key === "Escape") {
106
- if (searchOpen) {
107
- searchOpen = false;
108
- }
109
- event.preventDefault();
110
- }
111
- }
112
-
113
- beforeNavigate(() => {
114
- searchOpen = false;
115
- searchInput = "";
116
- });
117
-
118
- $effect(() => {
119
- if (searchOpen) {
120
- inputElement?.focus();
121
- }
122
- });
123
-
124
- $effect(() => {
125
- if (!searchOpen) {
126
- searchInput = "";
127
- debouncedInput = ""; // reset debouncedInput on search bar close
128
- }
129
- });
130
- </script>
131
-
132
- <svelte:window onkeydown={handleKeydown} onmousedown={handleBackdropClick} />
133
-
134
- {#if searchOpen}
135
- <div
136
- bind:this={searchContainer}
137
- class="fixed bottom-0 left-[5%] right-[5%] top-[10%] z-50
138
- m-4 mx-auto h-fit max-w-2xl
139
- overflow-hidden rounded-xl
140
- border border-gray-500/50 bg-gray-200 text-gray-800
141
- shadow-[0_10px_40px_rgba(100,100,100,0.2)]
142
- dark:bg-gray-800
143
- dark:text-gray-200 dark:shadow-[0_10px_40px_rgba(255,255,255,0.1)] lg:top-[20%]"
144
- in:fly={{ y: 100 }}
145
- >
146
- <button
147
- class="absolute right-1 top-2.5 rounded-full p-1 hover:bg-gray-500/50"
148
- onclick={toggleSearch}
149
- >
150
- <CarbonClose class="text-lg text-gray-400/80" />
151
- </button>
152
- <input
153
- bind:value={searchInput}
154
- bind:this={inputElement}
155
- type="text"
156
- name="searchbar"
157
- placeholder="Search for chats..."
158
- autocomplete="off"
159
- class={{
160
- "h-12 w-full p-4 text-lg dark:bg-gray-800 dark:text-gray-200": true,
161
- "border-b border-b-gray-500/50": searchInput && searchInput.length >= 3,
162
- }}
163
- />
164
-
165
- <div class="scrollbar-custom max-h-[40dvh] overflow-y-scroll">
166
- {#if debouncedInput && debouncedInput.length >= 3}
167
- {#if pending}
168
- {#each Array(5) as _}
169
- <div
170
- class="m-2 h-6 w-full animate-pulse gap-5 rounded bg-gray-300 first:mt-4 dark:bg-gray-700"
171
- ></div>
172
- {/each}
173
- {:else if conversations.length === 0}
174
- <p class="bg-gray-200 p-2 text-gray-700 dark:bg-gray-800 dark:text-gray-300">
175
- No conversations found matching that query
176
- </p>
177
- {:else}
178
- {#each Object.entries(groupedConversations) as [group, convs]}
179
- {#if convs.length}
180
- <h4 class="mb-1.5 mt-4 pl-1.5 text-sm text-gray-700 dark:text-gray-300">
181
- {titles[group]}
182
- </h4>
183
- {#each convs as conv}
184
- <NavConversationItem
185
- {conv}
186
- readOnly={true}
187
- showDescription={true}
188
- description={conv.content}
189
- searchInput={conv.matchedText}
190
- />
191
- {/each}
192
- {/if}
193
- {/each}
194
- {#if hasMore}
195
- <InfiniteScroll on:visible={() => handleVisible(searchInput)} />
196
- {/if}
197
- {/if}
198
- {/if}
199
- </div>
200
- </div>
201
- {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/api/routes/groups/conversations.ts CHANGED
@@ -8,8 +8,7 @@ import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversa
8
  import type { Conversation } from "$lib/types/Conversation";
9
 
10
  import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
11
- import pkg from "natural";
12
- const { PorterStemmer } = pkg;
13
 
14
  export const conversationGroup = new Elysia().use(authPlugin).group("/conversations", (app) => {
15
  return app
@@ -63,246 +62,7 @@ export const conversationGroup = new Elysia().use(authPlugin).group("/conversati
63
  });
64
  return res.deletedCount;
65
  })
66
- .get(
67
- "/search",
68
- async ({ locals, query }) => {
69
- const searchQuery = query.q;
70
- const p = query.p ?? 0;
71
-
72
- if (!searchQuery || searchQuery.length < 3) {
73
- return [];
74
- }
75
-
76
- if (!locals.user?._id && !locals.sessionId) {
77
- throw new Error("Must have a valid session or user");
78
- }
79
-
80
- const convs = await collections.conversations
81
- .find({
82
- sessionId: undefined,
83
- ...authCondition(locals),
84
- $text: { $search: searchQuery },
85
- })
86
- .sort({
87
- updatedAt: -1, // Sort by date updated in descending order
88
- })
89
- .project<
90
- Pick<Conversation, "_id" | "title" | "updatedAt" | "model" | "messages" | "userId">
91
- >({
92
- title: 1,
93
- updatedAt: 1,
94
- model: 1,
95
- messages: 1,
96
- userId: 1,
97
- })
98
- .skip(p * 5)
99
- .limit(5)
100
- .toArray()
101
- .then((convs) =>
102
- convs.map((conv) => {
103
- let matchedContent = "";
104
- let matchedText = "";
105
-
106
- // Find the best match using stemming to handle MongoDB's text search behavior
107
- let bestMatch = null;
108
- let bestMatchLength = 0;
109
-
110
- // Simple function to find the best match in content
111
- const findBestMatch = (
112
- content: string,
113
- query: string
114
- ): { start: number; end: number; text: string } | null => {
115
- const contentLower = content.toLowerCase();
116
- const queryLower = query.toLowerCase();
117
-
118
- // Try exact word boundary match first
119
- const wordRegex = new RegExp(
120
- `\\b${queryLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
121
- "gi"
122
- );
123
- const wordMatch = wordRegex.exec(content);
124
- if (wordMatch) {
125
- return {
126
- start: wordMatch.index,
127
- end: wordMatch.index + wordMatch[0].length - 1,
128
- text: wordMatch[0],
129
- };
130
- }
131
-
132
- // Try simple substring match
133
- const index = contentLower.indexOf(queryLower);
134
- if (index !== -1) {
135
- return {
136
- start: index,
137
- end: index + queryLower.length - 1,
138
- text: content.substring(index, index + queryLower.length),
139
- };
140
- }
141
-
142
- return null;
143
- };
144
-
145
- // Create search variations
146
- const searchVariations = [searchQuery.toLowerCase()];
147
-
148
- // Add stemmed variations
149
- try {
150
- const stemmed = PorterStemmer.stem(searchQuery.toLowerCase());
151
- if (stemmed !== searchQuery.toLowerCase()) {
152
- searchVariations.push(stemmed);
153
- }
154
-
155
- // Find actual words in conversations that stem to the same root
156
- for (const message of conv.messages) {
157
- if (message.content) {
158
- const words = message.content.toLowerCase().match(/\b\w+\b/g) || [];
159
- words.forEach((word: string) => {
160
- if (
161
- PorterStemmer.stem(word) === stemmed &&
162
- !searchVariations.includes(word)
163
- ) {
164
- searchVariations.push(word);
165
- }
166
- });
167
- }
168
- }
169
- } catch (e) {
170
- console.warn("Stemming failed for:", searchQuery, e);
171
- }
172
-
173
- // Add simple variations
174
- const query = searchQuery.toLowerCase();
175
- if (query.endsWith("s") && query.length > 3) {
176
- searchVariations.push(query.slice(0, -1));
177
- } else if (!query.endsWith("s")) {
178
- searchVariations.push(query + "s");
179
- }
180
-
181
- // Search through all messages for the best match
182
- for (const message of conv.messages) {
183
- if (!message.content) continue;
184
-
185
- // Try each variation in order of preference
186
- for (const variation of searchVariations) {
187
- const match = findBestMatch(message.content, variation);
188
- if (match) {
189
- const isExactQuery = variation === searchQuery.toLowerCase();
190
- const priority = isExactQuery ? 1000 : match.text.length;
191
-
192
- if (priority > bestMatchLength) {
193
- bestMatch = {
194
- content: message.content,
195
- matchStart: match.start,
196
- matchEnd: match.end,
197
- matchedText: match.text,
198
- };
199
- bestMatchLength = priority;
200
-
201
- // If we found exact query match, we're done
202
- if (isExactQuery) break;
203
- }
204
- }
205
- }
206
-
207
- // Stop if we found an exact match
208
- if (bestMatchLength >= 1000) break;
209
- }
210
-
211
- if (bestMatch) {
212
- const { content, matchStart, matchEnd } = bestMatch;
213
- matchedText = bestMatch.matchedText;
214
-
215
- // Create centered context around the match
216
- const maxContextLength = 160; // Maximum length of actual content (no padding)
217
- const matchLength = matchEnd - matchStart + 1;
218
-
219
- // Calculate context window - don't exceed maxContextLength even if content is longer
220
- const availableForContext =
221
- Math.min(maxContextLength, content.length) - matchLength;
222
- const contextPerSide = Math.floor(availableForContext / 2);
223
-
224
- // Calculate snippet boundaries to center the match within maxContextLength
225
- let snippetStart = Math.max(0, matchStart - contextPerSide);
226
- let snippetEnd = Math.min(
227
- content.length,
228
- matchStart + matchLength + contextPerSide
229
- );
230
-
231
- // Ensure we don't exceed maxContextLength
232
- if (snippetEnd - snippetStart > maxContextLength) {
233
- if (matchStart - contextPerSide < 0) {
234
- // Match is near start, extend end but limit to maxContextLength
235
- snippetEnd = Math.min(content.length, snippetStart + maxContextLength);
236
- } else {
237
- // Match is not near start, limit to maxContextLength from match start
238
- snippetEnd = Math.min(content.length, snippetStart + maxContextLength);
239
- }
240
- }
241
-
242
- // Adjust to word boundaries if possible (but don't move more than 15 chars)
243
- const originalStart = snippetStart;
244
- const originalEnd = snippetEnd;
245
-
246
- while (
247
- snippetStart > 0 &&
248
- content[snippetStart] !== " " &&
249
- content[snippetStart] !== "\n" &&
250
- originalStart - snippetStart < 15
251
- ) {
252
- snippetStart--;
253
- }
254
- while (
255
- snippetEnd < content.length &&
256
- content[snippetEnd] !== " " &&
257
- content[snippetEnd] !== "\n" &&
258
- snippetEnd - originalEnd < 15
259
- ) {
260
- snippetEnd++;
261
- }
262
-
263
- // Extract the content
264
- let extractedContent = content.substring(snippetStart, snippetEnd).trim();
265
- // Add ellipsis indicators only
266
- if (snippetStart > 0) {
267
- extractedContent = "..." + extractedContent;
268
- }
269
- if (snippetEnd < content.length) {
270
- extractedContent = extractedContent + "...";
271
- }
272
-
273
- matchedContent = extractedContent;
274
- } else {
275
- // Fallback: use beginning of the first message if no match found
276
- const firstMessage = conv.messages[0];
277
- if (firstMessage?.content) {
278
- const content = firstMessage.content;
279
- matchedContent =
280
- content.length > 200 ? content.substring(0, 200) + "..." : content;
281
- matchedText = searchQuery; // Fallback to search query
282
- }
283
- }
284
-
285
- return {
286
- _id: conv._id,
287
- id: conv._id,
288
- title: conv.title,
289
- content: matchedContent,
290
- matchedText,
291
- updatedAt: conv.updatedAt,
292
- model: conv.model,
293
- };
294
- })
295
- );
296
-
297
- return convs;
298
- },
299
- {
300
- query: t.Object({
301
- q: t.String(),
302
- p: t.Optional(t.Number()),
303
- }),
304
- }
305
- )
306
  .group(
307
  "/:id",
308
  {
 
8
  import type { Conversation } from "$lib/types/Conversation";
9
 
10
  import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
11
+ // search chat feature removed
 
12
 
13
  export const conversationGroup = new Elysia().use(authPlugin).group("/conversations", (app) => {
14
  return app
 
62
  });
63
  return res.deletedCount;
64
  })
65
+ // search endpoint removed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  .group(
67
  "/:id",
68
  {
src/routes/+layout.svelte CHANGED
@@ -20,7 +20,6 @@
20
  import { loginModalOpen } from "$lib/stores/loginModal";
21
  import LoginModal from "$lib/components/LoginModal.svelte";
22
  import OverloadedModal from "$lib/components/OverloadedModal.svelte";
23
- import Search from "$lib/components/chat/Search.svelte";
24
  import { setContext } from "svelte";
25
  import { handleResponse, useAPIClient } from "$lib/APIClient";
26
 
@@ -208,7 +207,6 @@
208
  <OverloadedModal onClose={() => (overloadedModalOpen = false)} />
209
  {/if}
210
 
211
- <Search />
212
 
213
  <div
214
  class="fixed grid h-full w-screen grid-cols-1 grid-rows-[auto,1fr] overflow-hidden text-smd {!isNavCollapsed
 
20
  import { loginModalOpen } from "$lib/stores/loginModal";
21
  import LoginModal from "$lib/components/LoginModal.svelte";
22
  import OverloadedModal from "$lib/components/OverloadedModal.svelte";
 
23
  import { setContext } from "svelte";
24
  import { handleResponse, useAPIClient } from "$lib/APIClient";
25
 
 
207
  <OverloadedModal onClose={() => (overloadedModalOpen = false)} />
208
  {/if}
209
 
 
210
 
211
  <div
212
  class="fixed grid h-full w-screen grid-cols-1 grid-rows-[auto,1fr] overflow-hidden text-smd {!isNavCollapsed