Browse Source

Merge pull request #9322 from jannikstdl/smart-suggestions

 feat: smart suggestions
Timothy Jaeryang Baek 2 months ago
parent
commit
0b01fd9d6f
2 changed files with 113 additions and 39 deletions
  1. 1 0
      src/lib/components/chat/Placeholder.svelte
  2. 112 39
      src/lib/components/chat/Suggestions.svelte

+ 1 - 0
src/lib/components/chat/Placeholder.svelte

@@ -220,6 +220,7 @@
 				suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
 					$config?.default_prompt_suggestions ??
 					[]}
+				inputValue={prompt}
 				on:select={(e) => {
 					selectSuggestionPrompt(e.detail);
 				}}

+ 112 - 39
src/lib/components/chat/Suggestions.svelte

@@ -1,4 +1,5 @@
 <script lang="ts">
+	import Fuse from 'fuse.js';
 	import Bolt from '$lib/components/icons/Bolt.svelte';
 	import { onMount, getContext, createEventDispatcher } from 'svelte';
 
@@ -7,47 +8,119 @@
 
 	export let suggestionPrompts = [];
 	export let className = '';
+	export let inputValue = '';
 
-	let prompts = [];
+	let sortedPrompts = [];
+	onMount(() => {
+		sortedPrompts = [...(suggestionPrompts ?? [])].sort(() => Math.random() - 0.5);
+	});
 
-	$: prompts = (suggestionPrompts ?? [])
-		.reduce((acc, current) => [...acc, ...[current]], [])
-		.sort(() => Math.random() - 0.5);
+	const fuseOptions = {
+		keys: ['content', 'title'],
+		threshold: 0.5
+	};
+
+	let fuse;
+	let filteredPrompts = [];
+	let oldFilteredPrompts = [];
+
+	// This variable controls the re-rendering of the suggestions
+	let version = 0;
+
+	// Initialize Fuse
+	$: fuse = new Fuse(sortedPrompts, fuseOptions);
+
+	// Update the filteredPrompts if inputValue changes
+	// Only increase version if something wirklich geändert hat
+	$: {
+		const newFilteredPrompts = inputValue.trim()
+			? fuse.search(inputValue.trim()).map((result) => result.item)
+			: sortedPrompts;
+
+		// Compare with the oldFilteredPrompts
+		// If there's a difference, update array + version
+		if (!arraysEqual(oldFilteredPrompts, newFilteredPrompts)) {
+			filteredPrompts = newFilteredPrompts;
+			version += 1;
+		}
+		oldFilteredPrompts = newFilteredPrompts;
+	}
+
+	// Helper function to check if arrays are the same
+	// (based on unique IDs oder content)
+	function arraysEqual(a, b) {
+		if (a.length !== b.length) return false;
+		for (let i = 0; i < a.length; i++) {
+			if ((a[i].id ?? a[i].content) !== (b[i].id ?? b[i].content)) {
+				return false;
+			}
+		}
+		return true;
+	}
 </script>
 
-{#if prompts.length > 0}
-	<div class="mb-1 flex gap-1 text-sm font-medium items-center text-gray-400 dark:text-gray-600">
-		<Bolt />
-		{$i18n.t('Suggested')}
-	</div>
-{/if}
-
-<div class=" h-40 max-h-full overflow-auto scrollbar-none {className}">
-	{#each prompts as prompt, promptIdx}
-		<button
-			class="flex flex-col flex-1 shrink-0 w-full justify-between px-3 py-2 rounded-xl bg-transparent hover:bg-black/5 dark:hover:bg-white/5 transition group"
-			on:click={() => {
-				dispatch('select', prompt.content);
-			}}
-		>
-			<div class="flex flex-col text-left">
-				{#if prompt.title && prompt.title[0] !== ''}
-					<div
-						class="  font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
-					>
-						{prompt.title[0]}
-					</div>
-					<div class="text-xs text-gray-500 font-normal line-clamp-1">{prompt.title[1]}</div>
-				{:else}
-					<div
-						class="  font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
-					>
-						{prompt.content}
-					</div>
-
-					<div class="text-xs text-gray-500 font-normal line-clamp-1">Prompt</div>
-				{/if}
-			</div>
-		</button>
-	{/each}
+<div
+	class="mb-1 flex gap-1 text-sm font-medium items-center text-gray-400 dark:text-gray-600"
+	style="visibility: {filteredPrompts.length > 0 ? 'visible' : 'hidden'}"
+>
+	<Bolt />
+	{$i18n.t('Suggested')}
+</div>
+
+<div class="h-40 overflow-auto scrollbar-none {className}">
+	{#if filteredPrompts.length > 0}
+		{#each filteredPrompts as prompt, idx ((prompt.id || prompt.content) + version)}
+			<button
+				class="waterfall-anim flex flex-col flex-1 shrink-0 w-full justify-between
+				       px-3 py-2 rounded-xl bg-transparent hover:bg-black/5
+				       dark:hover:bg-white/5 transition group"
+				style="animation-delay: {idx * 60}ms"
+				on:click={() => dispatch('select', prompt.content)}
+			>
+				<div class="flex flex-col text-left">
+					{#if prompt.title && prompt.title[0] !== ''}
+						<div
+							class="font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
+						>
+							{prompt.title[0]}
+						</div>
+						<div class="text-xs text-gray-500 font-normal line-clamp-1">
+							{prompt.title[1]}
+						</div>
+					{:else}
+						<div
+							class="font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
+						>
+							{prompt.content}
+						</div>
+						<div class="text-xs text-gray-500 font-normal line-clamp-1">{i18n.t('Prompt')}</div>
+					{/if}
+				</div>
+			</button>
+		{/each}
+	{:else}
+		<!-- Keine Vorschläge -->
+	{/if}
 </div>
+
+<style>
+	/* Waterfall animation for the suggestions */
+	@keyframes fadeInUp {
+		0% {
+			opacity: 0;
+			transform: translateY(20px);
+		}
+		100% {
+			opacity: 1;
+			transform: translateY(0);
+		}
+	}
+
+	.waterfall-anim {
+		opacity: 0;
+		animation-name: fadeInUp;
+		animation-duration: 200ms;
+		animation-fill-mode: forwards;
+		animation-timing-function: ease;
+	}
+</style>