mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 01:40:15 +01:00
169 lines
5.2 KiB
TypeScript
169 lines
5.2 KiB
TypeScript
|
|
import { memo, useState, useContext } from 'react';
|
||
|
|
import type { CitationProps } from './types';
|
||
|
|
import { SourceHovercard, FaviconImage, getCleanDomain } from '~/components/Web/SourceHovercard';
|
||
|
|
import { CitationContext, useCitation, useCompositeCitations } from './Context';
|
||
|
|
import { useLocalize } from '~/hooks';
|
||
|
|
|
||
|
|
interface CompositeCitationProps {
|
||
|
|
citationId?: string;
|
||
|
|
node?: {
|
||
|
|
properties?: CitationProps;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function CompositeCitation(props: CompositeCitationProps) {
|
||
|
|
const localize = useLocalize();
|
||
|
|
const { citations, citationId } = props.node?.properties ?? ({} as CitationProps);
|
||
|
|
const { setHoveredCitationId } = useContext(CitationContext);
|
||
|
|
const [currentPage, setCurrentPage] = useState(0);
|
||
|
|
const sources = useCompositeCitations(citations || []);
|
||
|
|
|
||
|
|
if (!sources || sources.length === 0) return null;
|
||
|
|
const totalPages = sources.length;
|
||
|
|
|
||
|
|
const getCitationLabel = () => {
|
||
|
|
if (!sources || sources.length === 0) return localize('com_citation_source');
|
||
|
|
|
||
|
|
const firstSource = sources[0];
|
||
|
|
const remainingCount = sources.length - 1;
|
||
|
|
const attribution =
|
||
|
|
firstSource.attribution ||
|
||
|
|
firstSource.title ||
|
||
|
|
getCleanDomain(firstSource.link || '') ||
|
||
|
|
localize('com_citation_source');
|
||
|
|
|
||
|
|
return remainingCount > 0 ? `${attribution} +${remainingCount}` : attribution;
|
||
|
|
};
|
||
|
|
|
||
|
|
const handlePrevPage = (e: React.MouseEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
e.stopPropagation();
|
||
|
|
if (currentPage > 0) {
|
||
|
|
setCurrentPage(currentPage - 1);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleNextPage = (e: React.MouseEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
e.stopPropagation();
|
||
|
|
if (currentPage < totalPages - 1) {
|
||
|
|
setCurrentPage(currentPage + 1);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const currentSource = sources?.[currentPage];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<SourceHovercard
|
||
|
|
source={currentSource}
|
||
|
|
label={getCitationLabel()}
|
||
|
|
onMouseEnter={() => setHoveredCitationId(citationId || null)}
|
||
|
|
onMouseLeave={() => setHoveredCitationId(null)}
|
||
|
|
>
|
||
|
|
{totalPages > 1 && (
|
||
|
|
<span className="mb-2 flex items-center justify-between border-b border-border-heavy pb-2">
|
||
|
|
<span className="flex gap-2">
|
||
|
|
<button
|
||
|
|
onClick={handlePrevPage}
|
||
|
|
disabled={currentPage === 0}
|
||
|
|
style={{ opacity: currentPage === 0 ? 0.5 : 1 }}
|
||
|
|
className="flex cursor-pointer items-center justify-center border-none bg-transparent p-0 text-base"
|
||
|
|
>
|
||
|
|
←
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={handleNextPage}
|
||
|
|
disabled={currentPage === totalPages - 1}
|
||
|
|
style={{ opacity: currentPage === totalPages - 1 ? 0.5 : 1 }}
|
||
|
|
className="flex cursor-pointer items-center justify-center border-none bg-transparent p-0 text-base"
|
||
|
|
>
|
||
|
|
→
|
||
|
|
</button>
|
||
|
|
</span>
|
||
|
|
<span className="text-xs text-text-tertiary">
|
||
|
|
{currentPage + 1}/{totalPages}
|
||
|
|
</span>
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
<span className="mb-2 flex items-center">
|
||
|
|
<FaviconImage domain={getCleanDomain(currentSource.link || '')} className="mr-2" />
|
||
|
|
<a
|
||
|
|
href={currentSource.link}
|
||
|
|
target="_blank"
|
||
|
|
rel="noopener noreferrer"
|
||
|
|
className="line-clamp-2 cursor-pointer overflow-hidden text-sm font-bold text-[#0066cc] hover:underline dark:text-blue-400 md:line-clamp-3"
|
||
|
|
>
|
||
|
|
{currentSource.attribution}
|
||
|
|
</a>
|
||
|
|
</span>
|
||
|
|
<h4 className="mb-1.5 mt-0 text-xs text-text-primary md:text-sm">{currentSource.title}</h4>
|
||
|
|
<p className="my-2 text-ellipsis break-all text-xs text-text-secondary md:text-sm">
|
||
|
|
{currentSource.snippet}
|
||
|
|
</p>
|
||
|
|
</SourceHovercard>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
interface CitationComponentProps {
|
||
|
|
citationId: string;
|
||
|
|
citationType: 'span' | 'standalone' | 'composite' | 'group' | 'navlist';
|
||
|
|
node?: {
|
||
|
|
properties?: CitationProps;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function Citation(props: CitationComponentProps) {
|
||
|
|
const localize = useLocalize();
|
||
|
|
const { citation, citationId } = props.node?.properties ?? {};
|
||
|
|
const { setHoveredCitationId } = useContext(CitationContext);
|
||
|
|
const refData = useCitation({
|
||
|
|
turn: citation?.turn || 0,
|
||
|
|
refType: citation?.refType,
|
||
|
|
index: citation?.index || 0,
|
||
|
|
});
|
||
|
|
if (!refData) return null;
|
||
|
|
|
||
|
|
const getCitationLabel = () => {
|
||
|
|
return (
|
||
|
|
refData.attribution ||
|
||
|
|
refData.title ||
|
||
|
|
getCleanDomain(refData.link || '') ||
|
||
|
|
localize('com_citation_source')
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<SourceHovercard
|
||
|
|
source={refData}
|
||
|
|
label={getCitationLabel()}
|
||
|
|
onMouseEnter={() => setHoveredCitationId(citationId || null)}
|
||
|
|
onMouseLeave={() => setHoveredCitationId(null)}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface HighlightedTextProps {
|
||
|
|
children: React.ReactNode;
|
||
|
|
citationId?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useHighlightState(citationId: string | undefined) {
|
||
|
|
const { hoveredCitationId } = useContext(CitationContext);
|
||
|
|
return citationId && hoveredCitationId === citationId;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const HighlightedText = memo(function HighlightedText({
|
||
|
|
children,
|
||
|
|
citationId,
|
||
|
|
}: HighlightedTextProps) {
|
||
|
|
const isHighlighted = useHighlightState(citationId);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<span
|
||
|
|
className={`rounded px-0 py-0.5 transition-colors ${isHighlighted ? 'bg-amber-300/20' : ''}`}
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</span>
|
||
|
|
);
|
||
|
|
});
|