mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-04 22:02:16 +02:00
feat: add frontend support for surfsense docs citations
This commit is contained in:
parent
3539b2a83d
commit
abd3bace53
4 changed files with 43 additions and 15 deletions
|
|
@ -7,13 +7,15 @@ import { SourceDetailPanel } from "@/components/new-chat/source-detail-panel";
|
||||||
interface InlineCitationProps {
|
interface InlineCitationProps {
|
||||||
chunkId: number;
|
chunkId: number;
|
||||||
citationNumber: number;
|
citationNumber: number;
|
||||||
|
isDocsChunk?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inline citation component for the new chat.
|
* Inline citation component for the new chat.
|
||||||
* Renders a clickable numbered badge that opens the SourceDetailPanel with document chunk details.
|
* Renders a clickable numbered badge that opens the SourceDetailPanel with document chunk details.
|
||||||
|
* Supports both regular knowledge base chunks and Surfsense documentation chunks.
|
||||||
*/
|
*/
|
||||||
export const InlineCitation: FC<InlineCitationProps> = ({ chunkId, citationNumber }) => {
|
export const InlineCitation: FC<InlineCitationProps> = ({ chunkId, citationNumber, isDocsChunk = false }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -21,10 +23,11 @@ export const InlineCitation: FC<InlineCitationProps> = ({ chunkId, citationNumbe
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onOpenChange={setIsOpen}
|
onOpenChange={setIsOpen}
|
||||||
chunkId={chunkId}
|
chunkId={chunkId}
|
||||||
sourceType=""
|
sourceType={isDocsChunk ? "SURFSENSE_DOCS" : ""}
|
||||||
title="Source"
|
title={isDocsChunk ? "Surfsense Documentation" : "Source"}
|
||||||
description=""
|
description=""
|
||||||
url=""
|
url=""
|
||||||
|
isDocsChunk={isDocsChunk}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsOpen(true)}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ import { InlineCitation } from "@/components/assistant-ui/inline-citation";
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
// Citation pattern: [citation:CHUNK_ID]
|
// Citation pattern: [citation:CHUNK_ID] or [citation:doc-CHUNK_ID]
|
||||||
const CITATION_REGEX = /\[citation:(\d+)\]/g;
|
const CITATION_REGEX = /\[citation:(doc-)?(\d+)\]/g;
|
||||||
|
|
||||||
// Track chunk IDs to citation numbers mapping for consistent numbering
|
// Track chunk IDs to citation numbers mapping for consistent numbering
|
||||||
// This map is reset when a new message starts rendering
|
// This map is reset when a new message starts rendering
|
||||||
|
|
@ -33,16 +33,20 @@ export function resetCitationCounter() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets or assigns a citation number for a chunk ID
|
* Gets or assigns a citation number for a chunk ID
|
||||||
|
* Uses string key to differentiate between doc and regular chunks
|
||||||
*/
|
*/
|
||||||
function getCitationNumber(chunkId: number): number {
|
function getCitationNumber(chunkId: number, isDocsChunk: boolean): number {
|
||||||
if (!chunkIdToCitationNumber.has(chunkId)) {
|
const key = isDocsChunk ? `doc-${chunkId}` : String(chunkId);
|
||||||
chunkIdToCitationNumber.set(chunkId, nextCitationNumber++);
|
const existingNumber = chunkIdToCitationNumber.get(key as unknown as number);
|
||||||
|
if (existingNumber === undefined) {
|
||||||
|
chunkIdToCitationNumber.set(key as unknown as number, nextCitationNumber++);
|
||||||
}
|
}
|
||||||
return chunkIdToCitationNumber.get(chunkId)!;
|
return chunkIdToCitationNumber.get(key as unknown as number)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses text and replaces [citation:XXX] patterns with InlineCitation components
|
* Parses text and replaces [citation:XXX] patterns with InlineCitation components
|
||||||
|
* Supports both regular chunks [citation:123] and docs chunks [citation:doc-123]
|
||||||
*/
|
*/
|
||||||
function parseTextWithCitations(text: string): ReactNode[] {
|
function parseTextWithCitations(text: string): ReactNode[] {
|
||||||
const parts: ReactNode[] = [];
|
const parts: ReactNode[] = [];
|
||||||
|
|
@ -59,14 +63,16 @@ function parseTextWithCitations(text: string): ReactNode[] {
|
||||||
parts.push(text.substring(lastIndex, match.index));
|
parts.push(text.substring(lastIndex, match.index));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the citation component
|
// Check if this is a docs chunk (has "doc-" prefix)
|
||||||
const chunkId = Number.parseInt(match[1], 10);
|
const isDocsChunk = match[1] === "doc-";
|
||||||
const citationNumber = getCitationNumber(chunkId);
|
const chunkId = Number.parseInt(match[2], 10);
|
||||||
|
const citationNumber = getCitationNumber(chunkId, isDocsChunk);
|
||||||
parts.push(
|
parts.push(
|
||||||
<InlineCitation
|
<InlineCitation
|
||||||
key={`citation-${chunkId}-${instanceIndex}`}
|
key={`citation-${isDocsChunk ? "doc-" : ""}${chunkId}-${instanceIndex}`}
|
||||||
chunkId={chunkId}
|
chunkId={chunkId}
|
||||||
citationNumber={citationNumber}
|
citationNumber={citationNumber}
|
||||||
|
isDocsChunk={isDocsChunk}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ interface SourceDetailPanelProps {
|
||||||
description?: string;
|
description?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
isDocsChunk?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDocumentType = (type: string) => {
|
const formatDocumentType = (type: string) => {
|
||||||
|
|
@ -114,6 +115,7 @@ export function SourceDetailPanel({
|
||||||
description,
|
description,
|
||||||
url,
|
url,
|
||||||
children,
|
children,
|
||||||
|
isDocsChunk = false,
|
||||||
}: SourceDetailPanelProps) {
|
}: SourceDetailPanelProps) {
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
const hasScrolledRef = useRef(false); // Use ref to avoid stale closures
|
const hasScrolledRef = useRef(false); // Use ref to avoid stale closures
|
||||||
|
|
@ -132,8 +134,13 @@ export function SourceDetailPanel({
|
||||||
isLoading: isDocumentByChunkFetching,
|
isLoading: isDocumentByChunkFetching,
|
||||||
error: documentByChunkFetchingError,
|
error: documentByChunkFetchingError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: cacheKeys.documents.byChunk(chunkId.toString()),
|
queryKey: isDocsChunk
|
||||||
queryFn: () => documentsApiService.getDocumentByChunk({ chunk_id: chunkId }),
|
? cacheKeys.documents.byChunk(`doc-${chunkId}`)
|
||||||
|
: cacheKeys.documents.byChunk(chunkId.toString()),
|
||||||
|
queryFn: () =>
|
||||||
|
isDocsChunk
|
||||||
|
? documentsApiService.getSurfsenseDocByChunk(chunkId)
|
||||||
|
: documentsApiService.getDocumentByChunk({ chunk_id: chunkId }),
|
||||||
enabled: !!chunkId && open,
|
enabled: !!chunkId && open,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,18 @@ class DocumentsApiService {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Surfsense documentation by chunk ID
|
||||||
|
* Used for resolving [citation:doc-XXX] citations
|
||||||
|
*/
|
||||||
|
getSurfsenseDocByChunk = async (chunkId: number) => {
|
||||||
|
// Response shape matches getDocumentByChunkResponse structure
|
||||||
|
return baseApiService.get(
|
||||||
|
`/api/v1/surfsense-docs/by-chunk/${chunkId}`,
|
||||||
|
getDocumentByChunkResponse
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a document
|
* Update a document
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue