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