b7f0f54aa0
* feat: add citation support in research report block and markdown - Enhanced ResearchReportBlock to fetch citations based on researchId and pass them to the Markdown component. - Introduced CitationLink component to display citation metadata on hover for links in markdown. - Implemented CitationCard and CitationList components for displaying citation details and lists. - Updated Markdown component to handle citation links and inline citations. - Created HoverCard component for displaying citation information in a tooltip-like manner. - Modified store to manage citations, including setting and retrieving citations for ongoing research. - Added CitationsEvent type to handle citations in chat events and updated Message type to include citations. * fix(log): Enable the logging level when enabling the DEBUG environment variable (#793) * fix(frontend): render all tool calls in the frontend #796 (#797) * build(deps): bump jspdf from 3.0.4 to 4.0.0 in /web (#798) Bumps [jspdf](https://github.com/parallax/jsPDF) from 3.0.4 to 4.0.0. - [Release notes](https://github.com/parallax/jsPDF/releases) - [Changelog](https://github.com/parallax/jsPDF/blob/master/RELEASE.md) - [Commits](https://github.com/parallax/jsPDF/compare/v3.0.4...v4.0.0) --- updated-dependencies: - dependency-name: jspdf dependency-version: 4.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(frontend):added the display of the 'analyst' message #800 (#801) * fix: migrate from deprecated create_react_agent to langchain.agents.create_agent (#802) * fix: migrate from deprecated create_react_agent to langchain.agents.create_agent Fixes #799 - Replace deprecated langgraph.prebuilt.create_react_agent with langchain.agents.create_agent (LangGraph 1.0 migration) - Add DynamicPromptMiddleware to handle dynamic prompt templates (replaces the 'prompt' callable parameter) - Add PreModelHookMiddleware to handle pre-model hooks (replaces the 'pre_model_hook' parameter) - Update AgentState import from langchain.agents in template.py - Update tests to use the new API * fix:update the code with review comments * fix: Add runtime parameter to compress_messages method(#803) * fix: Add runtime parameter to compress_messages method(#803) The compress_messages method was being called by PreModelHookMiddleware with both state and runtime parameters, but only accepted state parameter. This caused a TypeError when the middleware executed the pre_model_hook. Added optional runtime parameter to compress_messages signature to match the expected interface while maintaining backward compatibility. * Update the code with the review comments * fix: Refactor citation handling and add comprehensive tests for citation features * refactor: Clean up imports and formatting across citation modules * fix: Add monkeypatch to clear AGENT_RECURSION_LIMIT in recursion limit tests * feat: Enhance citation link handling in Markdown component * fix: Exclude citations from finish reason handling in mergeMessage function * fix(nodes): update message handling * fix(citations): improve citation extraction and handling in event processing * feat(citations): enhance citation extraction and handling with improved merging and normalization * fix(reporter): update citation formatting instructions for clarity and consistency * fix(reporter): prioritize using Markdown tables for data presentation and comparison --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: LoftyComet <1277173875@qq。> Co-authored-by: Willem Jiang <willem.jiang@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
309 lines
8.4 KiB
TypeScript
309 lines
8.4 KiB
TypeScript
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import { ExternalLink, Globe, Clock, Star } from "lucide-react";
|
|
import { useMemo } from "react";
|
|
|
|
import {
|
|
HoverCard,
|
|
HoverCardContent,
|
|
HoverCardTrigger,
|
|
} from "~/components/ui/hover-card";
|
|
import { cn } from "~/lib/utils";
|
|
import type { Citation } from "~/core/messages";
|
|
|
|
// Re-export Citation type as CitationData for backward compatibility
|
|
export type CitationData = Citation;
|
|
|
|
interface CitationLinkProps {
|
|
href: string;
|
|
children: React.ReactNode;
|
|
citations: CitationData[];
|
|
className?: string;
|
|
id?: string;
|
|
}
|
|
|
|
/**
|
|
* Enhanced link component that shows citation metadata on hover.
|
|
* Used within markdown content to provide rich citation information.
|
|
*/
|
|
export function CitationLink({
|
|
href,
|
|
children,
|
|
citations,
|
|
className,
|
|
id,
|
|
}: CitationLinkProps) {
|
|
// Find matching citation data for this URL
|
|
const { citation, index } = useMemo(() => {
|
|
if (!href || !citations) return { citation: null, index: -1 };
|
|
|
|
// Try exact match first
|
|
let matchIndex = citations.findIndex((c) => c.url === href);
|
|
|
|
// If not found, try versatile comparison using normalized URLs
|
|
if (matchIndex === -1) {
|
|
const normalizeUrl = (url: string) => {
|
|
try {
|
|
return decodeURIComponent(url).trim();
|
|
} catch {
|
|
return url.trim();
|
|
}
|
|
};
|
|
|
|
const normalizedHref = normalizeUrl(href);
|
|
|
|
matchIndex = citations.findIndex(
|
|
(c) => normalizeUrl(c.url) === normalizedHref
|
|
);
|
|
}
|
|
|
|
const match = matchIndex !== -1 ? citations[matchIndex] : null;
|
|
|
|
return { citation: match, index: matchIndex };
|
|
}, [href, citations]);
|
|
|
|
// If no citation data found, render as regular link
|
|
if (!citation) {
|
|
return (
|
|
<a
|
|
href={href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={cn("text-primary hover:underline", className)}
|
|
>
|
|
{children}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
const handleCitationClick = (e: React.MouseEvent) => {
|
|
// If it's an internal-looking citation (e.g. [1])
|
|
// or if the user clicks the citation number in the text
|
|
// we try to scroll to the reference list at the bottom
|
|
if (index !== -1) {
|
|
const targetId = `ref-${index + 1}`;
|
|
const element = document.getElementById(targetId);
|
|
if (element) {
|
|
e.preventDefault();
|
|
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
}
|
|
}
|
|
// If element not found or index is -1, let the default behavior (open URL) happen
|
|
};
|
|
|
|
return (
|
|
<HoverCard openDelay={200} closeDelay={100}>
|
|
<HoverCardTrigger asChild>
|
|
<a
|
|
id={id}
|
|
href={href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={handleCitationClick}
|
|
className={cn(
|
|
"text-primary hover:underline inline-flex items-center gap-0.5 cursor-pointer scroll-mt-20",
|
|
className
|
|
)}
|
|
>
|
|
{children}
|
|
<span className="text-xs text-muted-foreground ml-0.5">
|
|
<ExternalLink className="h-3 w-3 inline" />
|
|
</span>
|
|
</a>
|
|
</HoverCardTrigger>
|
|
<HoverCardContent
|
|
className="w-80 p-4"
|
|
side="top"
|
|
align="start"
|
|
sideOffset={8}
|
|
>
|
|
<CitationCard citation={citation} />
|
|
</HoverCardContent>
|
|
</HoverCard>
|
|
);
|
|
}
|
|
|
|
interface CitationCardProps {
|
|
citation: CitationData;
|
|
compact?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Card component displaying citation metadata.
|
|
*/
|
|
export function CitationCard({ citation, compact = false }: CitationCardProps) {
|
|
const {
|
|
title,
|
|
url,
|
|
description,
|
|
domain,
|
|
relevance_score,
|
|
accessed_at,
|
|
source_type,
|
|
} = citation;
|
|
|
|
// Format access date
|
|
const formattedDate = useMemo(() => {
|
|
if (!accessed_at) return null;
|
|
try {
|
|
const date = new Date(accessed_at);
|
|
return date.toLocaleDateString(undefined, {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "numeric",
|
|
});
|
|
} catch {
|
|
return accessed_at.slice(0, 10);
|
|
}
|
|
}, [accessed_at]);
|
|
|
|
// Format relevance score as percentage
|
|
const relevancePercent = useMemo(() => {
|
|
if (relevance_score == null || relevance_score <= 0) return null;
|
|
return Math.round(relevance_score * 100);
|
|
}, [relevance_score]);
|
|
|
|
return (
|
|
<span className={cn("block space-y-2", compact && "space-y-1")}>
|
|
{/* Title */}
|
|
<span className="block font-semibold text-sm line-clamp-2 leading-snug">
|
|
{title}
|
|
</span>
|
|
|
|
{/* Domain and metadata row */}
|
|
<span className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
{domain && (
|
|
<span className="flex items-center gap-1">
|
|
<Globe className="h-3 w-3" />
|
|
{domain}
|
|
</span>
|
|
)}
|
|
{formattedDate && (
|
|
<span className="flex items-center gap-1">
|
|
<Clock className="h-3 w-3" />
|
|
{formattedDate}
|
|
</span>
|
|
)}
|
|
{relevancePercent != null && (
|
|
<span className="flex items-center gap-1">
|
|
<Star className="h-3 w-3" />
|
|
{relevancePercent}% match
|
|
</span>
|
|
)}
|
|
</span>
|
|
|
|
{/* Description/snippet */}
|
|
{description && !compact && (
|
|
<span className="block text-xs text-muted-foreground line-clamp-3 leading-relaxed">
|
|
{description}
|
|
</span>
|
|
)}
|
|
|
|
{/* Source type badge */}
|
|
{source_type && (
|
|
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] bg-secondary text-secondary-foreground">
|
|
{source_type === "web_search" ? "Web" : source_type}
|
|
</span>
|
|
)}
|
|
|
|
{/* URL preview */}
|
|
<span className="block text-[10px] text-muted-foreground truncate opacity-60">
|
|
{url}
|
|
</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
interface CitationListProps {
|
|
citations: CitationData[];
|
|
title?: string;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* List component for displaying all citations.
|
|
*/
|
|
export function CitationList({
|
|
citations,
|
|
title = "Sources",
|
|
className,
|
|
}: CitationListProps) {
|
|
if (!citations || citations.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className={cn("space-y-3", className)}>
|
|
<h3 className="text-sm font-semibold text-foreground">{title}</h3>
|
|
<div className="space-y-2">
|
|
{citations.map((citation, index) => (
|
|
<div
|
|
key={citation.url || index}
|
|
className="p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary text-xs font-medium flex items-center justify-center">
|
|
{index + 1}
|
|
</span>
|
|
<div className="flex-1 min-w-0">
|
|
<a
|
|
href={citation.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-sm font-medium text-foreground hover:text-primary hover:underline line-clamp-1"
|
|
>
|
|
{citation.title}
|
|
</a>
|
|
{citation.domain && (
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
{citation.domain}
|
|
</p>
|
|
)}
|
|
{citation.description && (
|
|
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
|
{citation.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface CitationBadgeProps {
|
|
number: number;
|
|
citation?: CitationData;
|
|
onClick?: () => void;
|
|
}
|
|
|
|
/**
|
|
* Small numbered badge for inline citations.
|
|
*/
|
|
export function CitationBadge({ number, citation, onClick }: CitationBadgeProps) {
|
|
const badge = (
|
|
<button
|
|
onClick={onClick}
|
|
className="inline-flex items-center justify-center w-4 h-4 rounded-full bg-primary/10 text-primary text-[10px] font-medium hover:bg-primary/20 transition-colors align-super ml-0.5 cursor-pointer"
|
|
>
|
|
{number}
|
|
</button>
|
|
);
|
|
|
|
if (!citation) {
|
|
return badge;
|
|
}
|
|
|
|
return (
|
|
<HoverCard openDelay={200} closeDelay={100}>
|
|
<HoverCardTrigger asChild>{badge}</HoverCardTrigger>
|
|
<HoverCardContent className="w-72 p-3" side="top" sideOffset={4}>
|
|
<CitationCard citation={citation} compact />
|
|
</HoverCardContent>
|
|
</HoverCard>
|
|
);
|
|
}
|