feat: integrate Streamdown for markdown rendering and enhance citation handling

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-12-21 00:04:52 -08:00
parent 947087452f
commit 3906ba52e0
8 changed files with 1807 additions and 224 deletions

View file

@ -2,22 +2,22 @@
import type { FC } from "react";
import { useState } from "react";
import { SheetTrigger } from "@/components/ui/sheet";
import { SourceDetailSheet } from "@/components/chat/SourceDetailSheet";
import { SourceDetailPanel } from "@/components/new-chat/source-detail-panel";
interface InlineCitationProps {
chunkId: number;
citationNumber: number;
}
/**
* Inline citation component for the new chat.
* Renders a clickable badge that opens the SourceDetailSheet with document chunk details.
* Renders a clickable numbered badge that opens the SourceDetailPanel with document chunk details.
*/
export const InlineCitation: FC<InlineCitationProps> = ({ chunkId }) => {
export const InlineCitation: FC<InlineCitationProps> = ({ chunkId, citationNumber }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<SourceDetailSheet
<SourceDetailPanel
open={isOpen}
onOpenChange={setIsOpen}
chunkId={chunkId}
@ -26,22 +26,17 @@ export const InlineCitation: FC<InlineCitationProps> = ({ chunkId }) => {
description=""
url=""
>
<SheetTrigger asChild>
<span
className="text-[10px] font-bold bg-primary/80 hover:bg-primary text-primary-foreground rounded-full w-4 h-4 inline-flex items-center justify-center align-super cursor-pointer transition-colors ml-0.5"
title={`View source (chunk ${chunkId})`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
className="w-2.5 h-2.5"
>
<path d="M6.22 8.72a.75.75 0 0 0 1.06 1.06l5.22-5.22v1.69a.75.75 0 0 0 1.5 0v-3.5a.75.75 0 0 0-.75-.75h-3.5a.75.75 0 0 0 0 1.5h1.69L6.22 8.72Z" />
<path d="M3.5 6.75c0-.69.56-1.25 1.25-1.25H7A.75.75 0 0 0 7 4H4.75A2.75 2.75 0 0 0 2 6.75v4.5A2.75 2.75 0 0 0 4.75 14h4.5A2.75 2.75 0 0 0 12 11.25V9a.75.75 0 0 0-1.5 0v2.25c0 .69-.56 1.25-1.25 1.25h-4.5c-.69 0-1.25-.56-1.25-1.25v-4.5Z" />
</svg>
</span>
</SheetTrigger>
</SourceDetailSheet>
<span
onClick={() => setIsOpen(true)}
onKeyDown={(e) => e.key === "Enter" && setIsOpen(true)}
className="text-[10px] font-bold bg-primary/80 hover:bg-primary text-primary-foreground rounded-full min-w-4 h-4 px-1 inline-flex items-center justify-center align-super cursor-pointer transition-colors ml-0.5"
title={`View source #${citationNumber}`}
role="button"
tabIndex={0}
>
{citationNumber}
</span>
</SourceDetailPanel>
);
};

View file

@ -19,6 +19,29 @@ import { cn } from "@/lib/utils";
// Citation pattern: [citation:CHUNK_ID]
const CITATION_REGEX = /\[citation:(\d+)\]/g;
// Track chunk IDs to citation numbers mapping for consistent numbering
// This map is reset when a new message starts rendering
let chunkIdToCitationNumber: Map<number, number> = new Map();
let nextCitationNumber = 1;
/**
* Resets the citation counter - should be called at the start of each message
*/
export function resetCitationCounter() {
chunkIdToCitationNumber = new Map();
nextCitationNumber = 1;
}
/**
* Gets or assigns a citation number for a chunk ID
*/
function getCitationNumber(chunkId: number): number {
if (!chunkIdToCitationNumber.has(chunkId)) {
chunkIdToCitationNumber.set(chunkId, nextCitationNumber++);
}
return chunkIdToCitationNumber.get(chunkId)!;
}
/**
* Parses text and replaces [citation:XXX] patterns with InlineCitation components
*/
@ -26,7 +49,7 @@ function parseTextWithCitations(text: string): ReactNode[] {
const parts: ReactNode[] = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
let citationIndex = 0;
let instanceIndex = 0;
// Reset regex state
CITATION_REGEX.lastIndex = 0;
@ -39,12 +62,17 @@ function parseTextWithCitations(text: string): ReactNode[] {
// Add the citation component
const chunkId = Number.parseInt(match[1], 10);
const citationNumber = getCitationNumber(chunkId);
parts.push(
<InlineCitation key={`citation-${chunkId}-${citationIndex}`} chunkId={chunkId} />
<InlineCitation
key={`citation-${chunkId}-${instanceIndex}`}
chunkId={chunkId}
citationNumber={citationNumber}
/>
);
lastIndex = match.index + match[0].length;
citationIndex++;
instanceIndex++;
}
// Add any remaining text after the last citation
@ -56,6 +84,10 @@ function parseTextWithCitations(text: string): ReactNode[] {
}
const MarkdownTextImpl = () => {
// Reset citation counter at the start of each render
// This ensures consistent numbering as the message streams in
resetCitationCounter();
return (
<MarkdownTextPrimitive
remarkPlugins={[remarkGfm]}