mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-15 05:45:19 +00:00
Fix an issue where clicking inline citations in subsequent chat messages failed to open the citation modal when multiple collapsible sections are present.
The root cause was duplicate "collapsible-sources" IDs assigned to all Collapsible components. This led document.getElementById() to always return the first instance, preventing subsequent messages from opening their CitationModal.
Changes:
- Modify Collapsible ID generation in Citations.svelte to use unique IDs with "collapsible-${message.id}" pattern
- Update ResponseMessage.svelte's onSourceClick handler to reference the dynamic collapsible IDs
- Ensure proper citation modal binding for each chat message's sources
Affected components:
- Collapsible (expandable content sections)
- CitationsModal (citation detail popup)
This ensures each chat message's sources are independently collapsible and maintains proper citation modal binding throughout message history.
206 lines
6.2 KiB
Svelte
206 lines
6.2 KiB
Svelte
<script lang="ts">
|
|
import { getContext } from 'svelte';
|
|
import CitationsModal from './CitationsModal.svelte';
|
|
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
|
|
|
const i18n = getContext('i18n');
|
|
|
|
export let id = '';
|
|
export let sources = [];
|
|
|
|
let citations = [];
|
|
let showPercentage = false;
|
|
let showRelevance = true;
|
|
|
|
let showCitationModal = false;
|
|
let selectedCitation: any = null;
|
|
let isCollapsibleOpen = false;
|
|
|
|
function calculateShowRelevance(sources: any[]) {
|
|
const distances = sources.flatMap((citation) => citation.distances ?? []);
|
|
const inRange = distances.filter((d) => d !== undefined && d >= -1 && d <= 1).length;
|
|
const outOfRange = distances.filter((d) => d !== undefined && (d < -1 || d > 1)).length;
|
|
|
|
if (distances.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
(inRange === distances.length - 1 && outOfRange === 1) ||
|
|
(outOfRange === distances.length - 1 && inRange === 1)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function shouldShowPercentage(sources: any[]) {
|
|
const distances = sources.flatMap((citation) => citation.distances ?? []);
|
|
return distances.every((d) => d !== undefined && d >= -1 && d <= 1);
|
|
}
|
|
|
|
$: {
|
|
console.log('sources', sources);
|
|
citations = sources.reduce((acc, source) => {
|
|
if (Object.keys(source).length === 0) {
|
|
return acc;
|
|
}
|
|
|
|
source.document.forEach((document, index) => {
|
|
const metadata = source.metadata?.[index];
|
|
const distance = source.distances?.[index];
|
|
|
|
// Within the same citation there could be multiple documents
|
|
const id = metadata?.source ?? source?.source?.id ?? 'N/A';
|
|
let _source = source?.source;
|
|
|
|
if (metadata?.name) {
|
|
_source = { ..._source, name: metadata.name };
|
|
}
|
|
|
|
if (id.startsWith('http://') || id.startsWith('https://')) {
|
|
_source = { ..._source, name: id, url: id };
|
|
}
|
|
|
|
const existingSource = acc.find((item) => item.id === id);
|
|
|
|
if (existingSource) {
|
|
existingSource.document.push(document);
|
|
existingSource.metadata.push(metadata);
|
|
if (distance !== undefined) existingSource.distances.push(distance);
|
|
} else {
|
|
acc.push({
|
|
id: id,
|
|
source: _source,
|
|
document: [document],
|
|
metadata: metadata ? [metadata] : [],
|
|
distances: distance !== undefined ? [distance] : undefined
|
|
});
|
|
}
|
|
});
|
|
return acc;
|
|
}, []);
|
|
|
|
showRelevance = calculateShowRelevance(citations);
|
|
showPercentage = shouldShowPercentage(citations);
|
|
}
|
|
</script>
|
|
|
|
<CitationsModal
|
|
bind:show={showCitationModal}
|
|
citation={selectedCitation}
|
|
{showPercentage}
|
|
{showRelevance}
|
|
/>
|
|
|
|
{#if citations.length > 0}
|
|
<div class=" py-0.5 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
|
|
{#if citations.length <= 3}
|
|
<div class="flex text-xs font-medium flex-wrap">
|
|
{#each citations as citation, idx}
|
|
<button
|
|
id={`source-${id}-${idx}`}
|
|
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96"
|
|
on:click={() => {
|
|
showCitationModal = true;
|
|
selectedCitation = citation;
|
|
}}
|
|
>
|
|
{#if citations.every((c) => c.distances !== undefined)}
|
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
|
|
{idx + 1}
|
|
</div>
|
|
{/if}
|
|
<div
|
|
class="flex-1 mx-1 truncate text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
|
|
>
|
|
{citation.source.name}
|
|
</div>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<Collapsible
|
|
id={`collapsible-${id}`}
|
|
bind:open={isCollapsibleOpen}
|
|
className="w-full max-w-full "
|
|
buttonClassName="w-fit max-w-full"
|
|
>
|
|
<div
|
|
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"
|
|
>
|
|
<div
|
|
class="flex-1 flex items-center gap-1 overflow-auto scrollbar-none w-full max-w-full"
|
|
>
|
|
<span class="whitespace-nowrap hidden sm:inline shrink-0"
|
|
>{$i18n.t('References from')}</span
|
|
>
|
|
<div class="flex items-center overflow-auto scrollbar-none w-full max-w-full flex-1">
|
|
<div class="flex text-xs font-medium items-center">
|
|
{#each citations.slice(0, 2) as citation, idx}
|
|
<button
|
|
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"
|
|
on:click={() => {
|
|
showCitationModal = true;
|
|
selectedCitation = citation;
|
|
}}
|
|
on:pointerup={(e) => {
|
|
e.stopPropagation();
|
|
}}
|
|
>
|
|
{#if citations.every((c) => c.distances !== undefined)}
|
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
|
|
{idx + 1}
|
|
</div>
|
|
{/if}
|
|
<div class="flex-1 mx-1 truncate">
|
|
{citation.source.name}
|
|
</div>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-1 whitespace-nowrap shrink-0">
|
|
<span class="hidden sm:inline">{$i18n.t('and')}</span>
|
|
{citations.length - 2}
|
|
<span>{$i18n.t('more')}</span>
|
|
</div>
|
|
</div>
|
|
<div class="shrink-0">
|
|
{#if isCollapsibleOpen}
|
|
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
|
{:else}
|
|
<ChevronDown strokeWidth="3.5" className="size-3.5" />
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<div slot="content">
|
|
<div class="flex text-xs font-medium flex-wrap">
|
|
{#each citations as citation, idx}
|
|
<button
|
|
id={`source-${id}-${idx}`}
|
|
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"
|
|
on:click={() => {
|
|
showCitationModal = true;
|
|
selectedCitation = citation;
|
|
}}
|
|
>
|
|
{#if citations.every((c) => c.distances !== undefined)}
|
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
|
|
{idx + 1}
|
|
</div>
|
|
{/if}
|
|
<div class="flex-1 mx-1 truncate">
|
|
{citation.source.name}
|
|
</div>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</Collapsible>
|
|
{/if}
|
|
</div>
|
|
{/if}
|