feat: new chat working stateless. Added citation logic.

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-12-20 23:15:49 -08:00
parent 24f438a39e
commit 947087452f
10 changed files with 441 additions and 160 deletions

View file

@ -0,0 +1,47 @@
"use client";
import type { FC } from "react";
import { useState } from "react";
import { SheetTrigger } from "@/components/ui/sheet";
import { SourceDetailSheet } from "@/components/chat/SourceDetailSheet";
interface InlineCitationProps {
chunkId: number;
}
/**
* Inline citation component for the new chat.
* Renders a clickable badge that opens the SourceDetailSheet with document chunk details.
*/
export const InlineCitation: FC<InlineCitationProps> = ({ chunkId }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<SourceDetailSheet
open={isOpen}
onOpenChange={setIsOpen}
chunkId={chunkId}
sourceType=""
title="Source"
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>
);
};

View file

@ -9,12 +9,52 @@ import {
useIsMarkdownCodeBlock,
} from "@assistant-ui/react-markdown";
import { CheckIcon, CopyIcon } from "lucide-react";
import { type FC, memo, useState } from "react";
import { type FC, type ReactNode, memo, useState } from "react";
import remarkGfm from "remark-gfm";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { InlineCitation } from "@/components/assistant-ui/inline-citation";
import { cn } from "@/lib/utils";
// Citation pattern: [citation:CHUNK_ID]
const CITATION_REGEX = /\[citation:(\d+)\]/g;
/**
* Parses text and replaces [citation:XXX] patterns with InlineCitation components
*/
function parseTextWithCitations(text: string): ReactNode[] {
const parts: ReactNode[] = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
let citationIndex = 0;
// Reset regex state
CITATION_REGEX.lastIndex = 0;
while ((match = CITATION_REGEX.exec(text)) !== null) {
// Add text before the citation
if (match.index > lastIndex) {
parts.push(text.substring(lastIndex, match.index));
}
// Add the citation component
const chunkId = Number.parseInt(match[1], 10);
parts.push(
<InlineCitation key={`citation-${chunkId}-${citationIndex}`} chunkId={chunkId} />
);
lastIndex = match.index + match[0].length;
citationIndex++;
}
// Add any remaining text after the last citation
if (lastIndex < text.length) {
parts.push(text.substring(lastIndex));
}
return parts.length > 0 ? parts : [text];
}
const MarkdownTextImpl = () => {
return (
<MarkdownTextPrimitive
@ -60,63 +100,116 @@ const useCopyToClipboard = ({ copiedDuration = 3000 }: { copiedDuration?: number
return { isCopied, copyToClipboard };
};
/**
* Helper to process children and replace citation patterns with components
*/
function processChildrenWithCitations(children: ReactNode): ReactNode {
if (typeof children === "string") {
const parsed = parseTextWithCitations(children);
return parsed.length === 1 && typeof parsed[0] === "string" ? children : <>{parsed}</>;
}
if (Array.isArray(children)) {
return children.map((child, index) => {
if (typeof child === "string") {
const parsed = parseTextWithCitations(child);
return parsed.length === 1 && typeof parsed[0] === "string" ? (
child
) : (
<span key={index}>{parsed}</span>
);
}
return child;
});
}
return children;
}
const defaultComponents = memoizeMarkdownComponents({
h1: ({ className, ...props }) => (
h1: ({ className, children, ...props }) => (
<h1
className={cn(
"aui-md-h1 mb-8 scroll-m-20 font-extrabold text-4xl tracking-tight last:mb-0",
className
)}
{...props}
/>
>
{processChildrenWithCitations(children)}
</h1>
),
h2: ({ className, ...props }) => (
h2: ({ className, children, ...props }) => (
<h2
className={cn(
"aui-md-h2 mt-8 mb-4 scroll-m-20 font-semibold text-3xl tracking-tight first:mt-0 last:mb-0",
className
)}
{...props}
/>
>
{processChildrenWithCitations(children)}
</h2>
),
h3: ({ className, ...props }) => (
h3: ({ className, children, ...props }) => (
<h3
className={cn(
"aui-md-h3 mt-6 mb-4 scroll-m-20 font-semibold text-2xl tracking-tight first:mt-0 last:mb-0",
className
)}
{...props}
/>
>
{processChildrenWithCitations(children)}
</h3>
),
h4: ({ className, ...props }) => (
h4: ({ className, children, ...props }) => (
<h4
className={cn(
"aui-md-h4 mt-6 mb-4 scroll-m-20 font-semibold text-xl tracking-tight first:mt-0 last:mb-0",
className
)}
{...props}
/>
>
{processChildrenWithCitations(children)}
</h4>
),
h5: ({ className, ...props }) => (
h5: ({ className, children, ...props }) => (
<h5
className={cn("aui-md-h5 my-4 font-semibold text-lg first:mt-0 last:mb-0", className)}
{...props}
/>
>
{processChildrenWithCitations(children)}
</h5>
),
h6: ({ className, ...props }) => (
<h6 className={cn("aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0", className)} {...props} />
h6: ({ className, children, ...props }) => (
<h6
className={cn("aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0", className)}
{...props}
>
{processChildrenWithCitations(children)}
</h6>
),
p: ({ className, ...props }) => (
<p className={cn("aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0", className)} {...props} />
p: ({ className, children, ...props }) => (
<p
className={cn("aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0", className)}
{...props}
>
{processChildrenWithCitations(children)}
</p>
),
a: ({ className, ...props }) => (
a: ({ className, children, ...props }) => (
<a
className={cn("aui-md-a font-medium text-primary underline underline-offset-4", className)}
{...props}
/>
>
{processChildrenWithCitations(children)}
</a>
),
blockquote: ({ className, ...props }) => (
<blockquote className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)} {...props} />
blockquote: ({ className, children, ...props }) => (
<blockquote
className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)}
{...props}
>
{processChildrenWithCitations(children)}
</blockquote>
),
ul: ({ className, ...props }) => (
<ul className={cn("aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2", className)} {...props} />
@ -124,6 +217,11 @@ const defaultComponents = memoizeMarkdownComponents({
ol: ({ className, ...props }) => (
<ol className={cn("aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2", className)} {...props} />
),
li: ({ className, children, ...props }) => (
<li className={cn("aui-md-li", className)} {...props}>
{processChildrenWithCitations(children)}
</li>
),
hr: ({ className, ...props }) => (
<hr className={cn("aui-md-hr my-5 border-b", className)} {...props} />
),
@ -136,23 +234,27 @@ const defaultComponents = memoizeMarkdownComponents({
{...props}
/>
),
th: ({ className, ...props }) => (
th: ({ className, children, ...props }) => (
<th
className={cn(
"aui-md-th bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [[align=center]]:text-center [[align=right]]:text-right",
className
)}
{...props}
/>
>
{processChildrenWithCitations(children)}
</th>
),
td: ({ className, ...props }) => (
td: ({ className, children, ...props }) => (
<td
className={cn(
"aui-md-td border-b border-l px-4 py-2 text-left last:border-r [[align=center]]:text-center [[align=right]]:text-right",
className
)}
{...props}
/>
>
{processChildrenWithCitations(children)}
</td>
),
tr: ({ className, ...props }) => (
<tr
@ -187,5 +289,15 @@ const defaultComponents = memoizeMarkdownComponents({
/>
);
},
strong: ({ className, children, ...props }) => (
<strong className={cn("aui-md-strong font-semibold", className)} {...props}>
{processChildrenWithCitations(children)}
</strong>
),
em: ({ className, children, ...props }) => (
<em className={cn("aui-md-em", className)} {...props}>
{processChildrenWithCitations(children)}
</em>
),
CodeHeader,
});

View file

@ -118,7 +118,7 @@ const ThreadSuggestions: FC = () => {
className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-200"
style={{ animationDelay: `${100 + index * 50}ms` }}
>
<ThreadPrimitive.Suggestion prompt={suggestion.prompt} send asChild>
<ThreadPrimitive.Suggestion prompt={suggestion.prompt} autoSend asChild>
<Button
variant="ghost"
className="aui-thread-welcome-suggestion h-auto w-full @md:flex-col flex-wrap items-start justify-start gap-1 rounded-2xl border px-4 py-3 text-left text-sm transition-colors hover:bg-muted"