Citations.svelte 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  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 citations = [];
  9. let _citations = [];
  10. let showCitationModal = false;
  11. let selectedCitation = null;
  12. let isCollapsibleOpen = false;
  13. $: _citations = citations.reduce((acc, citation) => {
  14. citation.document.forEach((document, index) => {
  15. const metadata = citation.metadata?.[index];
  16. const distance = citation.distances?.[index];
  17. const id = metadata?.source ?? 'N/A';
  18. let source = citation?.source;
  19. if (metadata?.name) {
  20. source = { ...source, name: metadata.name };
  21. }
  22. // Check if ID looks like a URL
  23. if (id.startsWith('http://') || id.startsWith('https://')) {
  24. source = { name: id };
  25. }
  26. const existingSource = acc.find((item) => item.id === id);
  27. if (existingSource) {
  28. existingSource.document.push(document);
  29. existingSource.metadata.push(metadata);
  30. if (distance !== undefined) existingSource.distances.push(distance);
  31. } else {
  32. acc.push({
  33. id: id,
  34. source: source,
  35. document: [document],
  36. metadata: metadata ? [metadata] : [],
  37. distances: distance !== undefined ? [distance] : undefined
  38. });
  39. }
  40. });
  41. return acc;
  42. }, []);
  43. $: if (_citations.every((citation) => citation.distances !== undefined)) {
  44. // Sort citations by distance (relevance)
  45. _citations = _citations.sort((a, b) => {
  46. const aMinDistance = Math.min(...(a.distances ?? []));
  47. const bMinDistance = Math.min(...(b.distances ?? []));
  48. return aMinDistance - bMinDistance;
  49. });
  50. }
  51. </script>
  52. <CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
  53. {#if _citations.length > 0}
  54. <div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
  55. {#if _citations.length <= 3}
  56. {#each _citations as citation, idx}
  57. <div class="flex gap-1 text-xs font-semibold">
  58. <button
  59. class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
  60. on:click={() => {
  61. showCitationModal = true;
  62. selectedCitation = citation;
  63. }}
  64. >
  65. {#if _citations.every((c) => c.distances !== undefined)}
  66. <div class="bg-white dark:bg-gray-700 rounded-full size-4">
  67. {idx + 1}
  68. </div>
  69. {/if}
  70. <div class="flex-1 mx-2 line-clamp-1">
  71. {citation.source.name}
  72. </div>
  73. </button>
  74. </div>
  75. {/each}
  76. {:else}
  77. <Collapsible bind:open={isCollapsibleOpen} className="w-full">
  78. <div
  79. class="flex items-center gap-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
  80. >
  81. <span>{$i18n.t('References from')}</span>
  82. {#each _citations.slice(0, 2) as citation, idx}
  83. <div class="flex gap-1 text-xs font-semibold">
  84. <button
  85. class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition 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-white dark:bg-gray-700 rounded-full size-4">
  93. {idx + 1}
  94. </div>
  95. {/if}
  96. <div class="flex-1 mx-2 line-clamp-1">
  97. {citation.source.name}
  98. </div>
  99. </button>
  100. </div>
  101. {#if idx === 0}
  102. <span class="-ml-2">,</span>
  103. {/if}
  104. {/each}
  105. <span>{$i18n.t('and')}</span>
  106. <div class="text-gray-600 dark:text-gray-400">
  107. {_citations.length - 2}
  108. </div>
  109. <span>{$i18n.t('more')}</span>
  110. {#if isCollapsibleOpen}
  111. <ChevronUp strokeWidth="3.5" className="size-3.5" />
  112. {:else}
  113. <ChevronDown strokeWidth="3.5" className="size-3.5" />
  114. {/if}
  115. </div>
  116. <div slot="content" class="mt-2">
  117. <div class="flex flex-wrap gap-2">
  118. {#each _citations as citation, idx}
  119. <div class="flex gap-1 text-xs font-semibold">
  120. <button
  121. class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
  122. on:click={() => {
  123. showCitationModal = true;
  124. selectedCitation = citation;
  125. }}
  126. >
  127. {#if _citations.every((c) => c.distances !== undefined)}
  128. <div class="bg-white dark:bg-gray-700 rounded-full size-4">
  129. {idx + 1}
  130. </div>
  131. {/if}
  132. <div class="flex-1 mx-2 line-clamp-1">
  133. {citation.source.name}
  134. </div>
  135. </button>
  136. </div>
  137. {/each}
  138. </div>
  139. </div>
  140. </Collapsible>
  141. {/if}
  142. </div>
  143. {/if}