Citations.svelte 6.4 KB

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