mirror of
https://github.com/open-webui/open-webui.git
synced 2026-01-02 14:45:18 +00:00
refac: citation
This commit is contained in:
parent
2e9e46a9f1
commit
c0ec04935b
4 changed files with 69 additions and 30 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
export let overlay = false;
|
export let overlay = false;
|
||||||
|
|
||||||
const getSrcUrl = (url: string, chatId?: string, messageId?: string) => {
|
const getSrcUrl = (url: string, chatId?: string, messageId?: string, sourceId: string) => {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
|
|
||||||
|
|
@ -18,6 +18,10 @@
|
||||||
parsed.searchParams.set('message_id', messageId);
|
parsed.searchParams.set('message_id', messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sourceId) {
|
||||||
|
parsed.searchParams.set('source_id', sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
return parsed.toString();
|
return parsed.toString();
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback for relative URLs or invalid input
|
// Fallback for relative URLs or invalid input
|
||||||
|
|
@ -26,6 +30,7 @@
|
||||||
|
|
||||||
if (chatId) parts.push(`chat_id=${encodeURIComponent(chatId)}`);
|
if (chatId) parts.push(`chat_id=${encodeURIComponent(chatId)}`);
|
||||||
if (messageId) parts.push(`message_id=${encodeURIComponent(messageId)}`);
|
if (messageId) parts.push(`message_id=${encodeURIComponent(messageId)}`);
|
||||||
|
if (sourceId) parts.push(`source_id=${encodeURIComponent(sourceId)}`);
|
||||||
|
|
||||||
if (parts.length === 0) return url;
|
if (parts.length === 0) return url;
|
||||||
|
|
||||||
|
|
@ -68,7 +73,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<FullHeightIframe
|
<FullHeightIframe
|
||||||
src={getSrcUrl($embed?.url ?? '', $embed?.chatId, $embed?.messageId)}
|
src={getSrcUrl($embed?.url ?? '', $embed?.chatId, $embed?.messageId, $embed?.sourceId)}
|
||||||
payload={$embed?.source ?? null}
|
payload={$embed?.source ?? null}
|
||||||
iframeClassName="w-full h-full"
|
iframeClassName="w-full h-full"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,26 @@
|
||||||
|
|
||||||
let selectedCitation: any = null;
|
let selectedCitation: any = null;
|
||||||
|
|
||||||
export const showSourceModal = (sourceIdx) => {
|
export const showSourceModal = (sourceId) => {
|
||||||
if (citations[sourceIdx]) {
|
let index;
|
||||||
console.log('Showing citation modal for:', citations[sourceIdx]);
|
let suffix = null;
|
||||||
|
|
||||||
if (citations[sourceIdx]?.source?.embed_url) {
|
if (typeof sourceId === 'string') {
|
||||||
const embedUrl = citations[sourceIdx].source.embed_url;
|
const output = sourceId.split('#');
|
||||||
|
index = parseInt(output[0]) - 1;
|
||||||
|
|
||||||
|
if (output.length > 1) {
|
||||||
|
suffix = output[1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
index = sourceId - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (citations[index]) {
|
||||||
|
console.log('Showing citation modal for:', citations[index]);
|
||||||
|
|
||||||
|
if (citations[index]?.source?.embed_url) {
|
||||||
|
const embedUrl = citations[index].source.embed_url;
|
||||||
if (embedUrl) {
|
if (embedUrl) {
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
// Open in new tab if readOnly
|
// Open in new tab if readOnly
|
||||||
|
|
@ -39,18 +53,19 @@
|
||||||
showEmbeds.set(true);
|
showEmbeds.set(true);
|
||||||
embed.set({
|
embed.set({
|
||||||
url: embedUrl,
|
url: embedUrl,
|
||||||
title: citations[sourceIdx]?.source?.name || 'Embedded Content',
|
title: citations[index]?.source?.name || 'Embedded Content',
|
||||||
source: citations[sourceIdx],
|
source: citations[index],
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
messageId: id
|
messageId: id,
|
||||||
|
sourceId: sourceId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedCitation = citations[sourceIdx];
|
selectedCitation = citations[index];
|
||||||
showCitationModal = true;
|
showCitationModal = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedCitation = citations[sourceIdx];
|
selectedCitation = citations[index];
|
||||||
showCitationModal = true;
|
showCitationModal = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@
|
||||||
|
|
||||||
{#if sourceIds}
|
{#if sourceIds}
|
||||||
{#if (token?.ids ?? []).length == 1}
|
{#if (token?.ids ?? []).length == 1}
|
||||||
<Source id={token.ids[0] - 1} title={sourceIds[token.ids[0] - 1]} {onClick} />
|
{@const id = token.ids[0]}
|
||||||
|
{@const identifier = token.citationIdentifiers ? token.citationIdentifiers[0] : id - 1}
|
||||||
|
<Source id={identifier} title={sourceIds[id - 1]} {onClick} />
|
||||||
{:else}
|
{:else}
|
||||||
<LinkPreview.Root openDelay={0} bind:open={openPreview}>
|
<LinkPreview.Root openDelay={0} bind:open={openPreview}>
|
||||||
<LinkPreview.Trigger>
|
<LinkPreview.Trigger>
|
||||||
|
|
@ -65,9 +67,11 @@
|
||||||
el={containerElement}
|
el={containerElement}
|
||||||
>
|
>
|
||||||
<div class="bg-gray-50 dark:bg-gray-850 rounded-xl p-1 cursor-pointer">
|
<div class="bg-gray-50 dark:bg-gray-850 rounded-xl p-1 cursor-pointer">
|
||||||
{#each token.ids as sourceId}
|
{#each token.citationIdentifiers ?? token.ids as identifier}
|
||||||
|
{@const id =
|
||||||
|
typeof identifier === 'string' ? parseInt(identifier.split('#')[0]) : identifier}
|
||||||
<div class="">
|
<div class="">
|
||||||
<Source id={sourceId - 1} title={sourceIds[sourceId - 1]} {onClick} />
|
<Source id={identifier} title={sourceIds[id - 1]} {onClick} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,46 +4,61 @@ export function citationExtension() {
|
||||||
level: 'inline' as const,
|
level: 'inline' as const,
|
||||||
|
|
||||||
start(src: string) {
|
start(src: string) {
|
||||||
// Trigger on any [number]
|
// Trigger on any [number] or [number#suffix]
|
||||||
return src.search(/\[(\d[\d,\s]*)\]/);
|
// We check for a digit immediately after [ to avoid matching arbitrary links
|
||||||
|
return src.search(/\[\d/);
|
||||||
},
|
},
|
||||||
|
|
||||||
tokenizer(src: string) {
|
tokenizer(src: string) {
|
||||||
// Avoid matching footnotes
|
// Avoid matching footnotes
|
||||||
if (/^\[\^/.test(src)) return;
|
if (/^\[\^/.test(src)) return;
|
||||||
|
|
||||||
// Match ONE OR MORE adjacent [1] or [1,2] blocks
|
// Match ONE OR MORE adjacent [1], [1,2], or [1#foo] blocks
|
||||||
// Example matched: "[1][2,3][4]"
|
// Example matched: "[1][2,3][4#bar]"
|
||||||
const rule = /^(\[(?:\d[\d,\s]*)\])+/;
|
// We allow: digits, commas, spaces, and # followed by non-control chars (excluding ] and ,)
|
||||||
|
const rule = /^(\[(?:\d+(?:#[^,\]\s]+)?(?:,\s*\d+(?:#[^,\]\s]+)?)*)\])+/;
|
||||||
const match = rule.exec(src);
|
const match = rule.exec(src);
|
||||||
if (!match) return;
|
if (!match) return;
|
||||||
|
|
||||||
const raw = match[0];
|
const raw = match[0];
|
||||||
|
|
||||||
// Extract ALL bracket groups inside the big match
|
// Extract ALL bracket groups inside the big match
|
||||||
const groupRegex = /\[([\d,\s]+)\]/g;
|
const groupRegex = /\[([^\]]+)\]/g;
|
||||||
const ids: number[] = [];
|
const ids: number[] = [];
|
||||||
|
const citationIdentifiers: string[] = [];
|
||||||
let m: RegExpExecArray | null;
|
let m: RegExpExecArray | null;
|
||||||
|
|
||||||
while ((m = groupRegex.exec(raw))) {
|
while ((m = groupRegex.exec(raw))) {
|
||||||
const parsed = m[1]
|
// m[1] is the content inside brackets, e.g. "1, 2#foo"
|
||||||
.split(',')
|
const parts = m[1].split(',').map((p) => p.trim());
|
||||||
.map((n) => parseInt(n.trim(), 10))
|
|
||||||
.filter((n) => !isNaN(n));
|
|
||||||
|
|
||||||
ids.push(...parsed);
|
parts.forEach((part) => {
|
||||||
|
// Check if it starts with digit
|
||||||
|
const match = /^(\d+)(?:#(.+))?$/.exec(part);
|
||||||
|
if (match) {
|
||||||
|
const index = parseInt(match[1], 10);
|
||||||
|
if (!isNaN(index)) {
|
||||||
|
ids.push(index);
|
||||||
|
// Store the full identifier ("1#foo" or "1")
|
||||||
|
citationIdentifiers.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'citation',
|
type: 'citation',
|
||||||
raw,
|
raw,
|
||||||
ids // merged list
|
ids, // merged list of integers for legacy title lookup
|
||||||
|
citationIdentifiers // merged list of full identifiers for granular targeting
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
renderer(token: any) {
|
renderer(token: any) {
|
||||||
// e.g. "1,2,3"
|
// fallback text
|
||||||
return token.ids.join(',');
|
return token.raw;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue