diff --git a/surfsense_web/components/assistant-ui/inline-citation.tsx b/surfsense_web/components/assistant-ui/inline-citation.tsx index 065f37e8e..9eab9a3c3 100644 --- a/surfsense_web/components/assistant-ui/inline-citation.tsx +++ b/surfsense_web/components/assistant-ui/inline-citation.tsx @@ -7,13 +7,15 @@ import { SourceDetailPanel } from "@/components/new-chat/source-detail-panel"; interface InlineCitationProps { chunkId: number; citationNumber: number; + isDocsChunk?: boolean; } /** * Inline citation component for the new chat. * 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 = ({ chunkId, citationNumber }) => { +export const InlineCitation: FC = ({ chunkId, citationNumber, isDocsChunk = false }) => { const [isOpen, setIsOpen] = useState(false); return ( @@ -21,10 +23,11 @@ export const InlineCitation: FC = ({ chunkId, citationNumbe open={isOpen} onOpenChange={setIsOpen} chunkId={chunkId} - sourceType="" - title="Source" + sourceType={isDocsChunk ? "SURFSENSE_DOCS" : ""} + title={isDocsChunk ? "Surfsense Documentation" : "Source"} description="" url="" + isDocsChunk={isDocsChunk} > setIsOpen(true)} diff --git a/surfsense_web/components/assistant-ui/markdown-text.tsx b/surfsense_web/components/assistant-ui/markdown-text.tsx index 41d6143b9..532ae7663 100644 --- a/surfsense_web/components/assistant-ui/markdown-text.tsx +++ b/surfsense_web/components/assistant-ui/markdown-text.tsx @@ -15,8 +15,8 @@ import { InlineCitation } from "@/components/assistant-ui/inline-citation"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { cn } from "@/lib/utils"; -// Citation pattern: [citation:CHUNK_ID] -const CITATION_REGEX = /\[citation:(\d+)\]/g; +// Citation pattern: [citation:CHUNK_ID] or [citation:doc-CHUNK_ID] +const CITATION_REGEX = /\[citation:(doc-)?(\d+)\]/g; // Track chunk IDs to citation numbers mapping for consistent numbering // 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 + * Uses string key to differentiate between doc and regular chunks */ -function getCitationNumber(chunkId: number): number { - if (!chunkIdToCitationNumber.has(chunkId)) { - chunkIdToCitationNumber.set(chunkId, nextCitationNumber++); +function getCitationNumber(chunkId: number, isDocsChunk: boolean): number { + const key = isDocsChunk ? `doc-${chunkId}` : String(chunkId); + 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 + * Supports both regular chunks [citation:123] and docs chunks [citation:doc-123] */ function parseTextWithCitations(text: string): ReactNode[] { const parts: ReactNode[] = []; @@ -59,14 +63,16 @@ function parseTextWithCitations(text: string): ReactNode[] { parts.push(text.substring(lastIndex, match.index)); } - // Add the citation component - const chunkId = Number.parseInt(match[1], 10); - const citationNumber = getCitationNumber(chunkId); + // Check if this is a docs chunk (has "doc-" prefix) + const isDocsChunk = match[1] === "doc-"; + const chunkId = Number.parseInt(match[2], 10); + const citationNumber = getCitationNumber(chunkId, isDocsChunk); parts.push( ); diff --git a/surfsense_web/components/new-chat/source-detail-panel.tsx b/surfsense_web/components/new-chat/source-detail-panel.tsx index 35249dc50..dc0c3c3f8 100644 --- a/surfsense_web/components/new-chat/source-detail-panel.tsx +++ b/surfsense_web/components/new-chat/source-detail-panel.tsx @@ -34,6 +34,7 @@ interface SourceDetailPanelProps { description?: string; url?: string; children?: ReactNode; + isDocsChunk?: boolean; } const formatDocumentType = (type: string) => { @@ -114,6 +115,7 @@ export function SourceDetailPanel({ description, url, children, + isDocsChunk = false, }: SourceDetailPanelProps) { const scrollAreaRef = useRef(null); const hasScrolledRef = useRef(false); // Use ref to avoid stale closures @@ -132,8 +134,13 @@ export function SourceDetailPanel({ isLoading: isDocumentByChunkFetching, error: documentByChunkFetchingError, } = useQuery({ - queryKey: cacheKeys.documents.byChunk(chunkId.toString()), - queryFn: () => documentsApiService.getDocumentByChunk({ chunk_id: chunkId }), + queryKey: isDocsChunk + ? cacheKeys.documents.byChunk(`doc-${chunkId}`) + : cacheKeys.documents.byChunk(chunkId.toString()), + queryFn: () => + isDocsChunk + ? documentsApiService.getSurfsenseDocByChunk(chunkId) + : documentsApiService.getDocumentByChunk({ chunk_id: chunkId }), enabled: !!chunkId && open, staleTime: 5 * 60 * 1000, }); diff --git a/surfsense_web/lib/apis/documents-api.service.ts b/surfsense_web/lib/apis/documents-api.service.ts index cf7a4b778..372baee4d 100644 --- a/surfsense_web/lib/apis/documents-api.service.ts +++ b/surfsense_web/lib/apis/documents-api.service.ts @@ -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 */