Citations.svelte 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <script lang="ts">
  2. import { getContext } from 'svelte';
  3. import CitationsModal from './CitationsModal.svelte';
  4. import Collapsible from '$lib/components/common/Collapsible.svelte';
  5. import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
  6. import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
  7. const i18n = getContext('i18n');
  8. export let sources = [];
  9. let citations = [];
  10. let showPercentage = false;
  11. let showRelevance = true;
  12. let showCitationModal = false;
  13. let selectedCitation: any = null;
  14. let isCollapsibleOpen = false;
  15. function calculateShowRelevance(sources: any[]) {
  16. const distances = sources.flatMap((citation) => citation.distances ?? []);
  17. const inRange = distances.filter((d) => d !== undefined && d >= -1 && d <= 1).length;
  18. const outOfRange = distances.filter((d) => d !== undefined && (d < -1 || d > 1)).length;
  19. if (distances.length === 0) {
  20. return false;
  21. }
  22. if (
  23. (inRange === distances.length - 1 && outOfRange === 1) ||
  24. (outOfRange === distances.length - 1 && inRange === 1)
  25. ) {
  26. return false;
  27. }
  28. return true;
  29. }
  30. function shouldShowPercentage(sources: any[]) {
  31. const distances = sources.flatMap((citation) => citation.distances ?? []);
  32. return distances.every((d) => d !== undefined && d >= -1 && d <= 1);
  33. }
  34. $: {
  35. citations = sources.reduce((acc, source) => {
  36. if (Object.keys(source).length === 0) {
  37. return acc;
  38. }
  39. source.document.forEach((document, index) => {
  40. const metadata = source.metadata?.[index];
  41. const distance = source.distances?.[index];
  42. // Within the same citation there could be multiple documents
  43. const id = metadata?.source ?? 'N/A';
  44. let _source = source?.source;
  45. if (metadata?.name) {
  46. _source = { ..._source, name: metadata.name };
  47. }
  48. if (id.startsWith('http://') || id.startsWith('https://')) {
  49. _source = { ..._source, name: id, url: id };
  50. }
  51. const existingSource = acc.find((item) => item.id === id);
  52. if (existingSource) {
  53. existingSource.document.push(document);
  54. existingSource.metadata.push(metadata);
  55. if (distance !== undefined) existingSource.distances.push(distance);
  56. } else {
  57. acc.push({
  58. id: id,
  59. source: _source,
  60. document: [document],
  61. metadata: metadata ? [metadata] : [],
  62. distances: distance !== undefined ? [distance] : undefined
  63. });
  64. }
  65. });
  66. return acc;
  67. }, []);
  68. showRelevance = calculateShowRelevance(citations);
  69. showPercentage = shouldShowPercentage(citations);
  70. }
  71. </script>
  72. <CitationsModal
  73. bind:show={showCitationModal}
  74. citation={selectedCitation}
  75. {showPercentage}
  76. {showRelevance}
  77. />
  78. {#if citations.length > 0}
  79. <div class=" py-0.5 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
  80. {#if citations.length <= 3}
  81. <div class="flex text-xs font-medium flex-wrap">
  82. {#each citations as citation, idx}
  83. <button
  84. id={`source-${citation.source.name}`}
  85. class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96"
  86. on:click={() => {
  87. showCitationModal = true;
  88. selectedCitation = citation;
  89. }}
  90. >
  91. {#if citations.every((c) => c.distances !== undefined)}
  92. <div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
  93. {idx + 1}
  94. </div>
  95. {/if}
  96. <div
  97. class="flex-1 mx-1 truncate text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
  98. >
  99. {citation.source.name}
  100. </div>
  101. </button>
  102. {/each}
  103. </div>
  104. {:else}
  105. <Collapsible
  106. id="collapsible-sources"
  107. bind:open={isCollapsibleOpen}
  108. className="w-full max-w-full "
  109. buttonClassName="w-fit max-w-full"
  110. >
  111. <div
  112. class="flex w-full overflow-auto items-center gap-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
  113. >
  114. <div
  115. class="flex-1 flex items-center gap-1 overflow-auto scrollbar-none w-full max-w-full"
  116. >
  117. <span class="whitespace-nowrap hidden sm:inline flex-shrink-0"
  118. >{$i18n.t('References from')}</span
  119. >
  120. <div class="flex items-center overflow-auto scrollbar-none w-full max-w-full flex-1">
  121. <div class="flex text-xs font-medium items-center">
  122. {#each citations.slice(0, 2) as citation, idx}
  123. <button
  124. class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
  125. on:click={() => {
  126. showCitationModal = true;
  127. selectedCitation = citation;
  128. }}
  129. on:pointerup={(e) => {
  130. e.stopPropagation();
  131. }}
  132. >
  133. {#if citations.every((c) => c.distances !== undefined)}
  134. <div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
  135. {idx + 1}
  136. </div>
  137. {/if}
  138. <div class="flex-1 mx-1 truncate">
  139. {citation.source.name}
  140. </div>
  141. </button>
  142. {/each}
  143. </div>
  144. </div>
  145. <div class="flex items-center gap-1 whitespace-nowrap flex-shrink-0">
  146. <span class="hidden sm:inline">{$i18n.t('and')}</span>
  147. {citations.length - 2}
  148. <span>{$i18n.t('more')}</span>
  149. </div>
  150. </div>
  151. <div class="flex-shrink-0">
  152. {#if isCollapsibleOpen}
  153. <ChevronUp strokeWidth="3.5" className="size-3.5" />
  154. {:else}
  155. <ChevronDown strokeWidth="3.5" className="size-3.5" />
  156. {/if}
  157. </div>
  158. </div>
  159. <div slot="content">
  160. <div class="flex text-xs font-medium flex-wrap">
  161. {#each citations as citation, idx}
  162. <button
  163. id={`source-${citation.source.name}`}
  164. class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
  165. on:click={() => {
  166. showCitationModal = true;
  167. selectedCitation = citation;
  168. }}
  169. >
  170. {#if citations.every((c) => c.distances !== undefined)}
  171. <div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
  172. {idx + 1}
  173. </div>
  174. {/if}
  175. <div class="flex-1 mx-1 truncate">
  176. {citation.source.name}
  177. </div>
  178. </button>
  179. {/each}
  180. </div>
  181. </div>
  182. </Collapsible>
  183. {/if}
  184. </div>
  185. {/if}