refac: citation

This commit is contained in:
Timothy Jaeryang Baek 2025-12-26 02:05:03 +04:00
parent 2e9e46a9f1
commit c0ec04935b
4 changed files with 69 additions and 30 deletions

View file

@ -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"
/> />

View file

@ -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;
} }
} }

View file

@ -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>

View file

@ -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)); parts.forEach((part) => {
// Check if it starts with digit
ids.push(...parsed); 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;
} }
}; };
} }