Spaces:
Sleeping
Sleeping
[WIP] - Add the pop-up search (Ctrl/Command + K) to search entire conversation history (#1823)
Browse files* UI for crtl/command + k for conversation search
* UI for ctrl/command+k
* Added fetch conversations and filter conversations functions
* WIP - finish with the first draft of popup-search
* WIP - Finish the first version pop up search
* WIP - Finish the first version pop up search
* Remove unnecessary codes
* Fixed formatting errors
* Fixed formatting errors
* refactor: search functionality
* fix: revert those changes
* fix: lint
* chores: clean up unused comment
---------
Co-authored-by: kenneth.ramos95 <kenneth.ramos95@gmail.com>
Co-authored-by: Nathan Sarrazin <sarrazin.nathan@gmail.com>
- package-lock.json +8 -8
- package.json +2 -2
- src/lib/components/MobileNav.svelte +8 -1
- src/lib/components/NavConversationItem.svelte +61 -54
- src/lib/components/NavMenu.svelte +23 -8
- src/lib/components/chat/ChatWindow.svelte +1 -1
- src/lib/components/chat/Search.svelte +165 -0
- src/lib/server/database.ts +16 -0
- src/routes/+layout.svelte +3 -0
- src/routes/api/conversations/+server.ts +0 -2
- src/routes/api/conversations/search/+server.ts +59 -0
package-lock.json
CHANGED
|
@@ -93,12 +93,12 @@
|
|
| 93 |
"minimist": "^1.2.8",
|
| 94 |
"mongodb-memory-server": "^10.1.2",
|
| 95 |
"node-llama-cpp": "^3.6.0",
|
| 96 |
-
"prettier": "^3.
|
| 97 |
"prettier-plugin-svelte": "^3.2.6",
|
| 98 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
| 99 |
"prom-client": "^15.1.2",
|
| 100 |
"sade": "^1.8.1",
|
| 101 |
-
"svelte": "^5.27.
|
| 102 |
"svelte-check": "^4.0.0",
|
| 103 |
"svelte-gestures": "^5.1.3",
|
| 104 |
"ts-node": "^10.9.1",
|
|
@@ -12911,9 +12911,9 @@
|
|
| 12911 |
}
|
| 12912 |
},
|
| 12913 |
"node_modules/prettier": {
|
| 12914 |
-
"version": "3.
|
| 12915 |
-
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.
|
| 12916 |
-
"integrity": "sha512-
|
| 12917 |
"dev": true,
|
| 12918 |
"license": "MIT",
|
| 12919 |
"bin": {
|
|
@@ -14519,9 +14519,9 @@
|
|
| 14519 |
}
|
| 14520 |
},
|
| 14521 |
"node_modules/svelte": {
|
| 14522 |
-
"version": "5.27.
|
| 14523 |
-
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.27.
|
| 14524 |
-
"integrity": "sha512-
|
| 14525 |
"license": "MIT",
|
| 14526 |
"dependencies": {
|
| 14527 |
"@ampproject/remapping": "^2.3.0",
|
|
|
|
| 93 |
"minimist": "^1.2.8",
|
| 94 |
"mongodb-memory-server": "^10.1.2",
|
| 95 |
"node-llama-cpp": "^3.6.0",
|
| 96 |
+
"prettier": "^3.5.3",
|
| 97 |
"prettier-plugin-svelte": "^3.2.6",
|
| 98 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
| 99 |
"prom-client": "^15.1.2",
|
| 100 |
"sade": "^1.8.1",
|
| 101 |
+
"svelte": "^5.27.2",
|
| 102 |
"svelte-check": "^4.0.0",
|
| 103 |
"svelte-gestures": "^5.1.3",
|
| 104 |
"ts-node": "^10.9.1",
|
|
|
|
| 12911 |
}
|
| 12912 |
},
|
| 12913 |
"node_modules/prettier": {
|
| 12914 |
+
"version": "3.5.3",
|
| 12915 |
+
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
| 12916 |
+
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
| 12917 |
"dev": true,
|
| 12918 |
"license": "MIT",
|
| 12919 |
"bin": {
|
|
|
|
| 14519 |
}
|
| 14520 |
},
|
| 14521 |
"node_modules/svelte": {
|
| 14522 |
+
"version": "5.27.2",
|
| 14523 |
+
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.27.2.tgz",
|
| 14524 |
+
"integrity": "sha512-WQigWAJSONxMfIYFBnnKXf5s+TcXbktLsZ+HMLfjhjSHOW9C0OV9TmPPTxVdbxs5tuiS8iQecCxLTHpdgWwgiA==",
|
| 14525 |
"license": "MIT",
|
| 14526 |
"dependencies": {
|
| 14527 |
"@ampproject/remapping": "^2.3.0",
|
package.json
CHANGED
|
@@ -51,12 +51,12 @@
|
|
| 51 |
"minimist": "^1.2.8",
|
| 52 |
"mongodb-memory-server": "^10.1.2",
|
| 53 |
"node-llama-cpp": "^3.6.0",
|
| 54 |
-
"prettier": "^3.
|
| 55 |
"prettier-plugin-svelte": "^3.2.6",
|
| 56 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
| 57 |
"prom-client": "^15.1.2",
|
| 58 |
"sade": "^1.8.1",
|
| 59 |
-
"svelte": "^5.27.
|
| 60 |
"svelte-check": "^4.0.0",
|
| 61 |
"svelte-gestures": "^5.1.3",
|
| 62 |
"ts-node": "^10.9.1",
|
|
|
|
| 51 |
"minimist": "^1.2.8",
|
| 52 |
"mongodb-memory-server": "^10.1.2",
|
| 53 |
"node-llama-cpp": "^3.6.0",
|
| 54 |
+
"prettier": "^3.5.3",
|
| 55 |
"prettier-plugin-svelte": "^3.2.6",
|
| 56 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
| 57 |
"prom-client": "^15.1.2",
|
| 58 |
"sade": "^1.8.1",
|
| 59 |
+
"svelte": "^5.27.2",
|
| 60 |
"svelte-check": "^4.0.0",
|
| 61 |
"svelte-gestures": "^5.1.3",
|
| 62 |
"ts-node": "^10.9.1",
|
src/lib/components/MobileNav.svelte
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { browser } from "$app/environment";
|
| 3 |
import { beforeNavigate } from "$app/navigation";
|
|
@@ -18,7 +26,6 @@
|
|
| 18 |
let closeEl: HTMLButtonElement | undefined = $state();
|
| 19 |
let openEl: HTMLButtonElement | undefined = $state();
|
| 20 |
|
| 21 |
-
let isOpen = $state(false);
|
| 22 |
let panX: number | undefined = $state(undefined);
|
| 23 |
let panStart: number | undefined = $state(undefined);
|
| 24 |
let panStartTime: number | undefined = undefined;
|
|
|
|
| 1 |
+
<script lang="ts" module>
|
| 2 |
+
let isOpen = $state(false);
|
| 3 |
+
|
| 4 |
+
export function closeMobileNav() {
|
| 5 |
+
isOpen = false;
|
| 6 |
+
}
|
| 7 |
+
</script>
|
| 8 |
+
|
| 9 |
<script lang="ts">
|
| 10 |
import { browser } from "$app/environment";
|
| 11 |
import { beforeNavigate } from "$app/navigation";
|
|
|
|
| 26 |
let closeEl: HTMLButtonElement | undefined = $state();
|
| 27 |
let openEl: HTMLButtonElement | undefined = $state();
|
| 28 |
|
|
|
|
| 29 |
let panX: number | undefined = $state(undefined);
|
| 30 |
let panStart: number | undefined = $state(undefined);
|
| 31 |
let panStartTime: number | undefined = undefined;
|
src/lib/components/NavConversationItem.svelte
CHANGED
|
@@ -11,9 +11,10 @@
|
|
| 11 |
|
| 12 |
interface Props {
|
| 13 |
conv: ConvSidebar;
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
-
let { conv }: Props = $props();
|
| 17 |
|
| 18 |
let confirmDelete = $state(false);
|
| 19 |
|
|
@@ -55,59 +56,65 @@
|
|
| 55 |
{/if}
|
| 56 |
</div>
|
| 57 |
|
| 58 |
-
{#if
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
e
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
e
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
}}
|
| 79 |
-
>
|
| 80 |
-
<CarbonCheckmark class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
|
| 81 |
-
</button>
|
| 82 |
-
{:else}
|
| 83 |
-
<button
|
| 84 |
-
type="button"
|
| 85 |
-
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
| 86 |
-
title="Edit conversation title"
|
| 87 |
-
onclick={(e) => {
|
| 88 |
-
e.preventDefault();
|
| 89 |
-
const newTitle = prompt("Edit this conversation title:", conv.title);
|
| 90 |
-
if (!newTitle) return;
|
| 91 |
-
dispatch("editConversationTitle", { id: conv.id, title: newTitle });
|
| 92 |
-
}}
|
| 93 |
-
>
|
| 94 |
-
<CarbonEdit class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
|
| 95 |
-
</button>
|
| 96 |
-
|
| 97 |
-
<button
|
| 98 |
-
type="button"
|
| 99 |
-
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
| 100 |
-
title="Delete conversation"
|
| 101 |
-
onclick={(event) => {
|
| 102 |
-
event.preventDefault();
|
| 103 |
-
if (event.shiftKey) {
|
| 104 |
dispatch("deleteConversation", conv.id);
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
{/if}
|
| 113 |
</a>
|
|
|
|
| 11 |
|
| 12 |
interface Props {
|
| 13 |
conv: ConvSidebar;
|
| 14 |
+
readOnly?: true;
|
| 15 |
}
|
| 16 |
|
| 17 |
+
let { conv, readOnly }: Props = $props();
|
| 18 |
|
| 19 |
let confirmDelete = $state(false);
|
| 20 |
|
|
|
|
| 56 |
{/if}
|
| 57 |
</div>
|
| 58 |
|
| 59 |
+
{#if !readOnly}
|
| 60 |
+
{#if confirmDelete}
|
| 61 |
+
<button
|
| 62 |
+
type="button"
|
| 63 |
+
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
| 64 |
+
title="Cancel delete action"
|
| 65 |
+
onclick={(e) => {
|
| 66 |
+
e.preventDefault();
|
| 67 |
+
confirmDelete = false;
|
| 68 |
+
}}
|
| 69 |
+
>
|
| 70 |
+
<CarbonClose class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
|
| 71 |
+
</button>
|
| 72 |
+
<button
|
| 73 |
+
type="button"
|
| 74 |
+
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
| 75 |
+
title="Confirm delete action"
|
| 76 |
+
onclick={(e) => {
|
| 77 |
+
e.preventDefault();
|
| 78 |
+
confirmDelete = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
dispatch("deleteConversation", conv.id);
|
| 80 |
+
}}
|
| 81 |
+
>
|
| 82 |
+
<CarbonCheckmark
|
| 83 |
+
class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300"
|
| 84 |
+
/>
|
| 85 |
+
</button>
|
| 86 |
+
{:else}
|
| 87 |
+
<button
|
| 88 |
+
type="button"
|
| 89 |
+
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
| 90 |
+
title="Edit conversation title"
|
| 91 |
+
onclick={(e) => {
|
| 92 |
+
e.preventDefault();
|
| 93 |
+
const newTitle = prompt("Edit this conversation title:", conv.title);
|
| 94 |
+
if (!newTitle) return;
|
| 95 |
+
dispatch("editConversationTitle", { id: conv.id, title: newTitle });
|
| 96 |
+
}}
|
| 97 |
+
>
|
| 98 |
+
<CarbonEdit class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
|
| 99 |
+
</button>
|
| 100 |
+
|
| 101 |
+
<button
|
| 102 |
+
type="button"
|
| 103 |
+
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
| 104 |
+
title="Delete conversation"
|
| 105 |
+
onclick={(event) => {
|
| 106 |
+
event.preventDefault();
|
| 107 |
+
if (event.shiftKey) {
|
| 108 |
+
dispatch("deleteConversation", conv.id);
|
| 109 |
+
} else {
|
| 110 |
+
confirmDelete = true;
|
| 111 |
+
}
|
| 112 |
+
}}
|
| 113 |
+
>
|
| 114 |
+
<CarbonTrashCan
|
| 115 |
+
class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300"
|
| 116 |
+
/>
|
| 117 |
+
</button>
|
| 118 |
+
{/if}
|
| 119 |
{/if}
|
| 120 |
</a>
|
src/lib/components/NavMenu.svelte
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { base } from "$app/paths";
|
| 3 |
|
|
@@ -15,6 +24,9 @@
|
|
| 15 |
import type { Conversation } from "$lib/types/Conversation";
|
| 16 |
import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
|
| 17 |
import { browser } from "$app/environment";
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
interface Props {
|
| 20 |
conversations: ConvSidebar[];
|
|
@@ -48,13 +60,6 @@
|
|
| 48 |
older: conversations.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]),
|
| 49 |
});
|
| 50 |
|
| 51 |
-
const titles: { [key: string]: string } = {
|
| 52 |
-
today: "Today",
|
| 53 |
-
week: "This week",
|
| 54 |
-
month: "This month",
|
| 55 |
-
older: "Older",
|
| 56 |
-
} as const;
|
| 57 |
-
|
| 58 |
const nModels: number = $page.data.models.filter((el: Model) => !el.unlisted).length;
|
| 59 |
|
| 60 |
async function handleVisible() {
|
|
@@ -112,6 +117,16 @@
|
|
| 112 |
<div
|
| 113 |
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"
|
| 114 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
{#await groupedConversations}
|
| 116 |
{#if $page.data.nConversations > 0}
|
| 117 |
<div class="overflow-y-hidden">
|
|
@@ -220,7 +235,7 @@
|
|
| 220 |
theme = localStorage.theme;
|
| 221 |
}}
|
| 222 |
aria-label="Toggle theme"
|
| 223 |
-
class="flex h-9 min-w-[1.5em] flex-none items-center rounded-lg p-2
|
| 224 |
>
|
| 225 |
{#if browser}
|
| 226 |
{#if theme === "dark"}
|
|
|
|
| 1 |
+
<script lang="ts" module>
|
| 2 |
+
export const titles: { [key: string]: string } = {
|
| 3 |
+
today: "Today",
|
| 4 |
+
week: "This week",
|
| 5 |
+
month: "This month",
|
| 6 |
+
older: "Older",
|
| 7 |
+
} as const;
|
| 8 |
+
</script>
|
| 9 |
+
|
| 10 |
<script lang="ts">
|
| 11 |
import { base } from "$app/paths";
|
| 12 |
|
|
|
|
| 24 |
import type { Conversation } from "$lib/types/Conversation";
|
| 25 |
import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
|
| 26 |
import { browser } from "$app/environment";
|
| 27 |
+
import { toggleSearch } from "./chat/Search.svelte";
|
| 28 |
+
import CarbonSearch from "~icons/carbon/search";
|
| 29 |
+
import { closeMobileNav } from "./MobileNav.svelte";
|
| 30 |
|
| 31 |
interface Props {
|
| 32 |
conversations: ConvSidebar[];
|
|
|
|
| 60 |
older: conversations.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]),
|
| 61 |
});
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
const nModels: number = $page.data.models.filter((el: Model) => !el.unlisted).length;
|
| 64 |
|
| 65 |
async function handleVisible() {
|
|
|
|
| 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 |
+
<button
|
| 121 |
+
class="mx-auto flex w-full flex-row items-center justify-stretch gap-x-2 rounded-xl px-2 py-1 pl-0 align-middle text-gray-600 hover:bg-gray-500/20 dark:text-gray-400"
|
| 122 |
+
onclick={() => {
|
| 123 |
+
closeMobileNav();
|
| 124 |
+
toggleSearch();
|
| 125 |
+
}}
|
| 126 |
+
>
|
| 127 |
+
<CarbonSearch class="text-xs" />
|
| 128 |
+
<span class="block">Search chats</span></button
|
| 129 |
+
>
|
| 130 |
{#await groupedConversations}
|
| 131 |
{#if $page.data.nConversations > 0}
|
| 132 |
<div class="overflow-y-hidden">
|
|
|
|
| 235 |
theme = localStorage.theme;
|
| 236 |
}}
|
| 237 |
aria-label="Toggle theme"
|
| 238 |
+
class="flex h-9 min-w-[1.5em] flex-none items-center rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
| 239 |
>
|
| 240 |
{#if browser}
|
| 241 |
{#if theme === "dark"}
|
src/lib/components/chat/ChatWindow.svelte
CHANGED
|
@@ -247,7 +247,7 @@
|
|
| 247 |
}}
|
| 248 |
/>
|
| 249 |
|
| 250 |
-
<div class="relative min-h-0 min-w-0">
|
| 251 |
<div
|
| 252 |
class="scrollbar-custom h-full overflow-y-auto"
|
| 253 |
use:snapScrollToBottom={messages.map((message) => message.content)}
|
|
|
|
| 247 |
}}
|
| 248 |
/>
|
| 249 |
|
| 250 |
+
<div class="relative z-[-1] min-h-0 min-w-0">
|
| 251 |
<div
|
| 252 |
class="scrollbar-custom h-full overflow-y-auto"
|
| 253 |
use:snapScrollToBottom={messages.map((message) => message.content)}
|
src/lib/components/chat/Search.svelte
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 { base } from "$app/paths";
|
| 11 |
+
|
| 12 |
+
import { debounce } from "$lib/utils/debounce";
|
| 13 |
+
import { onDestroy, onMount } from "svelte";
|
| 14 |
+
|
| 15 |
+
import type { GETSearchEndpointReturn } from "../../../routes/api/conversations/search/+server";
|
| 16 |
+
import NavConversationItem from "../NavConversationItem.svelte";
|
| 17 |
+
import { titles } from "../NavMenu.svelte";
|
| 18 |
+
import { beforeNavigate } from "$app/navigation";
|
| 19 |
+
import { browser } from "$app/environment";
|
| 20 |
+
|
| 21 |
+
import CarbonClose from "~icons/carbon/close";
|
| 22 |
+
import { fly } from "svelte/transition";
|
| 23 |
+
|
| 24 |
+
let inputElement: HTMLInputElement | undefined = $state(undefined);
|
| 25 |
+
|
| 26 |
+
let searchInput: string = $state("");
|
| 27 |
+
let debouncedInput: string = $state("");
|
| 28 |
+
|
| 29 |
+
let pending: boolean = $state(false);
|
| 30 |
+
|
| 31 |
+
let conversations: GETSearchEndpointReturn = $state([]);
|
| 32 |
+
|
| 33 |
+
const dateRanges = [
|
| 34 |
+
new Date().setDate(new Date().getDate() - 1),
|
| 35 |
+
new Date().setDate(new Date().getDate() - 7),
|
| 36 |
+
new Date().setMonth(new Date().getMonth() - 1),
|
| 37 |
+
];
|
| 38 |
+
|
| 39 |
+
let groupedConversations = $derived({
|
| 40 |
+
today: conversations.filter(({ updatedAt }) => updatedAt.getTime() > dateRanges[0]),
|
| 41 |
+
week: conversations.filter(
|
| 42 |
+
({ updatedAt }) => updatedAt.getTime() > dateRanges[1] && updatedAt.getTime() < dateRanges[0]
|
| 43 |
+
),
|
| 44 |
+
month: conversations.filter(
|
| 45 |
+
({ updatedAt }) => updatedAt.getTime() > dateRanges[2] && updatedAt.getTime() < dateRanges[1]
|
| 46 |
+
),
|
| 47 |
+
older: conversations.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]),
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
const update = debounce(async (v: string) => {
|
| 51 |
+
debouncedInput = v;
|
| 52 |
+
pending = true;
|
| 53 |
+
await fetch(`${base}/api/conversations/search?q=${v}`)
|
| 54 |
+
.then(async (r) => {
|
| 55 |
+
if (r.ok) {
|
| 56 |
+
conversations = await r.json().then((conversations) =>
|
| 57 |
+
conversations.map((conv: GETSearchEndpointReturn[number]) => ({
|
| 58 |
+
...conv,
|
| 59 |
+
updatedAt: new Date(conv.updatedAt),
|
| 60 |
+
}))
|
| 61 |
+
);
|
| 62 |
+
} else {
|
| 63 |
+
conversations = [];
|
| 64 |
+
}
|
| 65 |
+
})
|
| 66 |
+
.finally(() => {
|
| 67 |
+
pending = false;
|
| 68 |
+
});
|
| 69 |
+
}, 300);
|
| 70 |
+
|
| 71 |
+
$effect(() => update(searchInput));
|
| 72 |
+
|
| 73 |
+
async function openSearchListener(ev: KeyboardEvent) {
|
| 74 |
+
if (ev.ctrlKey && ev.key === "k") {
|
| 75 |
+
searchOpen = true;
|
| 76 |
+
ev.stopPropagation();
|
| 77 |
+
ev.preventDefault();
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
onMount(() => {
|
| 82 |
+
if (!browser) return;
|
| 83 |
+
window.addEventListener("keydown", openSearchListener);
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
beforeNavigate(() => {
|
| 87 |
+
searchOpen = false;
|
| 88 |
+
searchInput = "";
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
onDestroy(() => {
|
| 92 |
+
if (!browser) return;
|
| 93 |
+
window.removeEventListener("keydown", openSearchListener);
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
$effect(() => {
|
| 97 |
+
if (searchOpen) {
|
| 98 |
+
inputElement?.focus();
|
| 99 |
+
}
|
| 100 |
+
});
|
| 101 |
+
|
| 102 |
+
$effect(() => {
|
| 103 |
+
if (!searchOpen) {
|
| 104 |
+
searchInput = "";
|
| 105 |
+
}
|
| 106 |
+
});
|
| 107 |
+
</script>
|
| 108 |
+
|
| 109 |
+
{#if searchOpen}
|
| 110 |
+
<div
|
| 111 |
+
class="fixed bottom-0 left-[5%] right-[5%] top-[10%] z-50
|
| 112 |
+
m-4 mx-auto h-fit max-w-2xl
|
| 113 |
+
overflow-hidden rounded-xl
|
| 114 |
+
border border-gray-500/50 bg-gray-200 text-gray-800
|
| 115 |
+
shadow-[0_10px_40px_rgba(100,100,100,0.2)]
|
| 116 |
+
dark:bg-gray-800
|
| 117 |
+
dark:text-gray-200 dark:shadow-[0_10px_40px_rgba(255,255,255,0.1)] lg:top-[20%]"
|
| 118 |
+
in:fly={{ y: 100 }}
|
| 119 |
+
>
|
| 120 |
+
<button
|
| 121 |
+
class="absolute right-1 top-2.5 rounded-full p-1 hover:bg-gray-500/50"
|
| 122 |
+
onclick={toggleSearch}
|
| 123 |
+
>
|
| 124 |
+
<CarbonClose class="text-lg text-gray-400/80" />
|
| 125 |
+
</button>
|
| 126 |
+
<input
|
| 127 |
+
bind:value={searchInput}
|
| 128 |
+
bind:this={inputElement}
|
| 129 |
+
type="text"
|
| 130 |
+
name="searchbar"
|
| 131 |
+
placeholder="Search for chats..."
|
| 132 |
+
class={{
|
| 133 |
+
"h-12 w-full p-4 text-lg dark:bg-gray-800 dark:text-gray-200": true,
|
| 134 |
+
"border-b border-b-gray-500/50": searchInput && searchInput.length >= 3,
|
| 135 |
+
}}
|
| 136 |
+
/>
|
| 137 |
+
|
| 138 |
+
<div class="max-h-[50dvh] overflow-y-scroll">
|
| 139 |
+
{#if debouncedInput && debouncedInput.length >= 3}
|
| 140 |
+
{#if pending}
|
| 141 |
+
{#each Array(5) as _}
|
| 142 |
+
<div
|
| 143 |
+
class="m-2 h-6 w-full animate-pulse gap-5 rounded bg-gray-300 first:mt-4 dark:bg-gray-700"
|
| 144 |
+
></div>
|
| 145 |
+
{/each}
|
| 146 |
+
{:else if conversations.length === 0}
|
| 147 |
+
<p class="bg-gray-200 p-2 text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
| 148 |
+
No conversations found matching that query
|
| 149 |
+
</p>
|
| 150 |
+
{:else}
|
| 151 |
+
{#each Object.entries(groupedConversations) as [group, convs]}
|
| 152 |
+
{#if convs.length}
|
| 153 |
+
<h4 class="mb-1.5 mt-4 pl-1.5 text-sm text-gray-400 dark:text-gray-500">
|
| 154 |
+
{titles[group]}
|
| 155 |
+
</h4>
|
| 156 |
+
{#each convs as conv}
|
| 157 |
+
<NavConversationItem {conv} readOnly={true} />
|
| 158 |
+
{/each}
|
| 159 |
+
{/if}
|
| 160 |
+
{/each}
|
| 161 |
+
{/if}
|
| 162 |
+
{/if}
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
{/if}
|
src/lib/server/database.ts
CHANGED
|
@@ -198,6 +198,22 @@ export class Database {
|
|
| 198 |
conversations
|
| 199 |
.createIndex({ "messages.createdAt": 1 }, { sparse: true })
|
| 200 |
.catch((e) => logger.error(e));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
// Unique index for stats
|
| 202 |
conversationStats
|
| 203 |
.createIndex(
|
|
|
|
| 198 |
conversations
|
| 199 |
.createIndex({ "messages.createdAt": 1 }, { sparse: true })
|
| 200 |
.catch((e) => logger.error(e));
|
| 201 |
+
|
| 202 |
+
// Text index for searching conversation titles and message content
|
| 203 |
+
conversations
|
| 204 |
+
.createIndex(
|
| 205 |
+
{
|
| 206 |
+
userId: 1,
|
| 207 |
+
sessionId: 1,
|
| 208 |
+
title: "text",
|
| 209 |
+
"messages.content": "text",
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
default_language: "en",
|
| 213 |
+
}
|
| 214 |
+
)
|
| 215 |
+
.catch((e) => logger.error(e));
|
| 216 |
+
|
| 217 |
// Unique index for stats
|
| 218 |
conversationStats
|
| 219 |
.createIndex(
|
src/routes/+layout.svelte
CHANGED
|
@@ -22,6 +22,7 @@
|
|
| 22 |
import { loginModalOpen } from "$lib/stores/loginModal";
|
| 23 |
import LoginModal from "$lib/components/LoginModal.svelte";
|
| 24 |
import OverloadedModal from "$lib/components/OverloadedModal.svelte";
|
|
|
|
| 25 |
|
| 26 |
let { data = $bindable(), children } = $props();
|
| 27 |
|
|
@@ -261,6 +262,8 @@
|
|
| 261 |
<OverloadedModal onClose={() => (overloadedModalOpen = false)} />
|
| 262 |
{/if}
|
| 263 |
|
|
|
|
|
|
|
| 264 |
<div
|
| 265 |
class="fixed grid h-full w-screen grid-cols-1 grid-rows-[auto,1fr] overflow-hidden text-smd {!isNavCollapsed
|
| 266 |
? 'md:grid-cols-[290px,1fr]'
|
|
|
|
| 22 |
import { loginModalOpen } from "$lib/stores/loginModal";
|
| 23 |
import LoginModal from "$lib/components/LoginModal.svelte";
|
| 24 |
import OverloadedModal from "$lib/components/OverloadedModal.svelte";
|
| 25 |
+
import Search from "$lib/components/chat/Search.svelte";
|
| 26 |
|
| 27 |
let { data = $bindable(), children } = $props();
|
| 28 |
|
|
|
|
| 262 |
<OverloadedModal onClose={() => (overloadedModalOpen = false)} />
|
| 263 |
{/if}
|
| 264 |
|
| 265 |
+
<Search />
|
| 266 |
+
|
| 267 |
<div
|
| 268 |
class="fixed grid h-full w-screen grid-cols-1 grid-rows-[auto,1fr] overflow-hidden text-smd {!isNavCollapsed
|
| 269 |
? 'md:grid-cols-[290px,1fr]'
|
src/routes/api/conversations/+server.ts
CHANGED
|
@@ -6,7 +6,6 @@ import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
|
|
| 6 |
|
| 7 |
export async function GET({ locals, url }) {
|
| 8 |
const p = parseInt(url.searchParams.get("p") ?? "0");
|
| 9 |
-
|
| 10 |
if (locals.user?._id || locals.sessionId) {
|
| 11 |
const convs = await collections.conversations
|
| 12 |
.find({
|
|
@@ -26,7 +25,6 @@ export async function GET({ locals, url }) {
|
|
| 26 |
if (convs.length === 0) {
|
| 27 |
return Response.json([]);
|
| 28 |
}
|
| 29 |
-
|
| 30 |
const res = convs.map((conv) => ({
|
| 31 |
_id: conv._id,
|
| 32 |
id: conv._id, // legacy param iOS
|
|
|
|
| 6 |
|
| 7 |
export async function GET({ locals, url }) {
|
| 8 |
const p = parseInt(url.searchParams.get("p") ?? "0");
|
|
|
|
| 9 |
if (locals.user?._id || locals.sessionId) {
|
| 10 |
const convs = await collections.conversations
|
| 11 |
.find({
|
|
|
|
| 25 |
if (convs.length === 0) {
|
| 26 |
return Response.json([]);
|
| 27 |
}
|
|
|
|
| 28 |
const res = convs.map((conv) => ({
|
| 29 |
_id: conv._id,
|
| 30 |
id: conv._id, // legacy param iOS
|
src/routes/api/conversations/search/+server.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
|
| 2 |
+
import { authCondition } from "$lib/server/auth";
|
| 3 |
+
import { collections } from "$lib/server/database";
|
| 4 |
+
import { models } from "$lib/server/models";
|
| 5 |
+
import type { RequestHandler } from "@sveltejs/kit";
|
| 6 |
+
|
| 7 |
+
export type GETSearchEndpointReturn = Array<{
|
| 8 |
+
id: string;
|
| 9 |
+
title: string;
|
| 10 |
+
updatedAt: Date;
|
| 11 |
+
model: string;
|
| 12 |
+
assistantId?: string;
|
| 13 |
+
mdoelTools?: boolean;
|
| 14 |
+
}>;
|
| 15 |
+
|
| 16 |
+
export const GET: RequestHandler = async ({ locals, url }) => {
|
| 17 |
+
const searchQuery = url.searchParams.get("q");
|
| 18 |
+
const p = parseInt(url.searchParams.get("p") ?? "0");
|
| 19 |
+
|
| 20 |
+
if (!searchQuery || searchQuery.length < 3) {
|
| 21 |
+
return Response.json([]);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
if (locals.user?._id || locals.sessionId) {
|
| 25 |
+
const convs = await collections.conversations
|
| 26 |
+
.find({
|
| 27 |
+
...authCondition(locals),
|
| 28 |
+
$text: { $search: searchQuery },
|
| 29 |
+
})
|
| 30 |
+
.sort({ score: { $meta: "textScore" } })
|
| 31 |
+
.project({
|
| 32 |
+
title: 1,
|
| 33 |
+
updatedAt: 1,
|
| 34 |
+
model: 1,
|
| 35 |
+
assistantId: 1,
|
| 36 |
+
messages: 1,
|
| 37 |
+
userId: 1,
|
| 38 |
+
})
|
| 39 |
+
.skip(p * CONV_NUM_PER_PAGE)
|
| 40 |
+
.limit(CONV_NUM_PER_PAGE)
|
| 41 |
+
.toArray()
|
| 42 |
+
.then((convs) =>
|
| 43 |
+
convs.map((conv) => {
|
| 44 |
+
return {
|
| 45 |
+
_id: conv._id,
|
| 46 |
+
id: conv._id, // legacy param iOS
|
| 47 |
+
title: conv.title,
|
| 48 |
+
updatedAt: conv.updatedAt,
|
| 49 |
+
model: conv.model,
|
| 50 |
+
assistantId: conv.assistantId,
|
| 51 |
+
modelTools: models.find((m) => m.id == conv.model)?.tools ?? false,
|
| 52 |
+
};
|
| 53 |
+
})
|
| 54 |
+
);
|
| 55 |
+
|
| 56 |
+
return Response.json(convs as GETSearchEndpointReturn);
|
| 57 |
+
}
|
| 58 |
+
return Response.json([]);
|
| 59 |
+
};
|