Suggestions.svelte 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. <script lang="ts">
  2. import Fuse from 'fuse.js';
  3. import Bolt from '$lib/components/icons/Bolt.svelte';
  4. import { onMount, getContext, createEventDispatcher } from 'svelte';
  5. const i18n = getContext('i18n');
  6. const dispatch = createEventDispatcher();
  7. export let suggestionPrompts = [];
  8. export let className = '';
  9. export let inputValue = '';
  10. let sortedPrompts = [];
  11. onMount(() => {
  12. sortedPrompts = [...(suggestionPrompts ?? [])].sort(() => Math.random() - 0.5);
  13. });
  14. const fuseOptions = {
  15. keys: ['content', 'title'],
  16. threshold: 0.5
  17. };
  18. let fuse;
  19. let filteredPrompts = [];
  20. let oldFilteredPrompts = [];
  21. // This variable controls the re-rendering of the suggestions
  22. let version = 0;
  23. // Initialize Fuse
  24. $: fuse = new Fuse(sortedPrompts, fuseOptions);
  25. // Update the filteredPrompts if inputValue changes
  26. // Only increase version if something wirklich geändert hat
  27. $: {
  28. const newFilteredPrompts = inputValue.trim()
  29. ? fuse.search(inputValue.trim()).map((result) => result.item)
  30. : sortedPrompts;
  31. // Compare with the oldFilteredPrompts
  32. // If there's a difference, update array + version
  33. if (!arraysEqual(oldFilteredPrompts, newFilteredPrompts)) {
  34. filteredPrompts = newFilteredPrompts;
  35. version += 1;
  36. }
  37. oldFilteredPrompts = newFilteredPrompts;
  38. }
  39. // Helper function to check if arrays are the same
  40. // (based on unique IDs oder content)
  41. function arraysEqual(a, b) {
  42. if (a.length !== b.length) return false;
  43. for (let i = 0; i < a.length; i++) {
  44. if ((a[i].id ?? a[i].content) !== (b[i].id ?? b[i].content)) {
  45. return false;
  46. }
  47. }
  48. return true;
  49. }
  50. </script>
  51. <div
  52. class="mb-1 flex gap-1 text-sm font-medium items-center text-gray-400 dark:text-gray-600"
  53. style="visibility: {filteredPrompts.length > 0 ? 'visible' : 'hidden'}"
  54. >
  55. <Bolt />
  56. {$i18n.t('Suggested')}
  57. </div>
  58. <div class="h-40 overflow-auto scrollbar-none {className}">
  59. {#if filteredPrompts.length > 0}
  60. {#each filteredPrompts as prompt, idx ((prompt.id || prompt.content) + version)}
  61. <button
  62. class="waterfall-anim flex flex-col flex-1 shrink-0 w-full justify-between
  63. px-3 py-2 rounded-xl bg-transparent hover:bg-black/5
  64. dark:hover:bg-white/5 transition group"
  65. style="animation-delay: {idx * 60}ms"
  66. on:click={() => dispatch('select', prompt.content)}
  67. >
  68. <div class="flex flex-col text-left">
  69. {#if prompt.title && prompt.title[0] !== ''}
  70. <div
  71. class="font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
  72. >
  73. {prompt.title[0]}
  74. </div>
  75. <div class="text-xs text-gray-500 font-normal line-clamp-1">
  76. {prompt.title[1]}
  77. </div>
  78. {:else}
  79. <div
  80. class="font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
  81. >
  82. {prompt.content}
  83. </div>
  84. <div class="text-xs text-gray-500 font-normal line-clamp-1">{i18n.t('Prompt')}</div>
  85. {/if}
  86. </div>
  87. </button>
  88. {/each}
  89. {:else}
  90. <!-- Keine Vorschläge -->
  91. {/if}
  92. </div>
  93. <style>
  94. /* Waterfall animation for the suggestions */
  95. @keyframes fadeInUp {
  96. 0% {
  97. opacity: 0;
  98. transform: translateY(20px);
  99. }
  100. 100% {
  101. opacity: 1;
  102. transform: translateY(0);
  103. }
  104. }
  105. .waterfall-anim {
  106. opacity: 0;
  107. animation-name: fadeInUp;
  108. animation-duration: 200ms;
  109. animation-fill-mode: forwards;
  110. animation-timing-function: ease;
  111. }
  112. </style>