feat: refactor crawler trust link style (#166)

* feat: refactor crawler trust link style

* feat: enhance link credibility checks in Markdown and related components
This commit is contained in:
Leo Hui
2025-05-15 17:17:10 +08:00
committed by GitHub
parent 8802eea0ba
commit a43db94fb6
5 changed files with 53 additions and 35 deletions

View File

@@ -75,7 +75,9 @@ function ActivityMessage({ messageId }: { messageId: string }) {
if (message.agent !== "reporter" && message.agent !== "planner") { if (message.agent !== "reporter" && message.agent !== "planner") {
return ( return (
<div className="px-4 py-2"> <div className="px-4 py-2">
<Markdown animated>{message.content}</Markdown> <Markdown animated checkLinkCredibility>
{message.content}
</Markdown>
</div> </div>
); );
} }

View File

@@ -64,7 +64,9 @@ export function ResearchReportBlock({
/> />
) : ( ) : (
<> <>
<Markdown animated>{message?.content}</Markdown> <Markdown animated checkLinkCredibility>
{message?.content}
</Markdown>
{message?.isStreaming && <LoadingAnimation className="my-12" />} {message?.isStreaming && <LoadingAnimation className="my-12" />}
</> </>
)} )}

View File

@@ -1,18 +1,24 @@
import { useEffect, useMemo } from "react"; import { useMemo } from "react";
import { useToolCalls } from "~/core/store"; import { useStore, useToolCalls } from "~/core/store";
import { cn } from "~/lib/utils";
import { Tooltip } from "./tooltip"; import { Tooltip } from "./tooltip";
import { WarningFilled } from "@ant-design/icons";
export const Link = ({ export const Link = ({
href, href,
children, children,
checkLinkCredibility = false,
}: { }: {
href: string | undefined; href: string | undefined;
children: React.ReactNode; children: React.ReactNode;
checkLinkCredibility: boolean;
}) => { }) => {
const toolCalls = useToolCalls(); const toolCalls = useToolCalls();
const responding = useStore((state) => state.responding);
const credibleLinks = useMemo(() => { const credibleLinks = useMemo(() => {
const links = new Set<string>(); const links = new Set<string>();
if (!checkLinkCredibility) return links;
(toolCalls || []).forEach((call) => { (toolCalls || []).forEach((call) => {
if (call && call.name === "web_search" && call.result) { if (call && call.name === "web_search" && call.result) {
const result = JSON.parse(call.result) as Array<{ url: string }>; const result = JSON.parse(call.result) as Array<{ url: string }>;
@@ -23,28 +29,26 @@ export const Link = ({
}); });
return links; return links;
}, [toolCalls]); }, [toolCalls]);
const isCredible = useMemo(() => {
return href ? credibleLinks.has(href) : true;
}, [credibleLinks, href]);
if (!isCredible) { const isCredible = useMemo(() => {
return ( return checkLinkCredibility && href && !responding
<Tooltip title="This link might be a hallucination from AI model and may not be reliable."> ? credibleLinks.has(href)
<a : true;
href={href} }, [credibleLinks, href, responding, checkLinkCredibility]);
target="_blank"
rel="noopener noreferrer"
className={cn(isCredible && "after:ml-0.5 after:content-['⚠️']")}
>
{children}
</a>
</Tooltip>
);
}
return ( return (
<a href={href} target="_blank" rel="noopener noreferrer"> <span className="flex items-center gap-1.5">
{children} <a href={href} target="_blank" rel="noopener noreferrer">
</a> {children}
</a>
{!isCredible && (
<Tooltip
title="This link might be a hallucination from AI model and may not be reliable."
delayDuration={300}
>
<WarningFilled className="text-sx transition-colors hover:!text-yellow-500" />
</Tooltip>
)}
</span>
); );
}; };

View File

@@ -20,28 +20,36 @@ import Image from "./image";
import { Tooltip } from "./tooltip"; import { Tooltip } from "./tooltip";
import { Link } from "./link"; import { Link } from "./link";
const components: ReactMarkdownOptions["components"] = {
a: ({ href, children }) => <Link href={href}>{children}</Link>,
img: ({ src, alt }) => (
<a href={src as string} target="_blank" rel="noopener noreferrer">
<Image className="rounded" src={src as string} alt={alt ?? ""} />
</a>
),
};
export function Markdown({ export function Markdown({
className, className,
children, children,
style, style,
enableCopy, enableCopy,
animated = false, animated = false,
checkLinkCredibility = false,
...props ...props
}: ReactMarkdownOptions & { }: ReactMarkdownOptions & {
className?: string; className?: string;
enableCopy?: boolean; enableCopy?: boolean;
style?: React.CSSProperties; style?: React.CSSProperties;
animated?: boolean; animated?: boolean;
checkLinkCredibility?: boolean;
}) { }) {
const components: ReactMarkdownOptions["components"] = useMemo(() => {
return {
a: ({ href, children }) => (
<Link href={href} checkLinkCredibility={checkLinkCredibility}>
{children}
</Link>
),
img: ({ src, alt }) => (
<a href={src as string} target="_blank" rel="noopener noreferrer">
<Image className="rounded" src={src as string} alt={alt ?? ""} />
</a>
),
};
}, [checkLinkCredibility]);
const rehypePlugins = useMemo(() => { const rehypePlugins = useMemo(() => {
if (animated) { if (animated) {
return [rehypeKatex, rehypeSplitWordsIntoSpans]; return [rehypeKatex, rehypeSplitWordsIntoSpans];

View File

@@ -19,6 +19,7 @@ export function Tooltip({
open, open,
side, side,
sideOffset, sideOffset,
delayDuration = 750,
}: { }: {
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
@@ -27,10 +28,11 @@ export function Tooltip({
open?: boolean; open?: boolean;
side?: "left" | "right" | "top" | "bottom"; side?: "left" | "right" | "top" | "bottom";
sideOffset?: number; sideOffset?: number;
delayDuration?: number;
}) { }) {
return ( return (
<TooltipProvider> <TooltipProvider>
<ShadcnTooltip delayDuration={750} open={open}> <ShadcnTooltip delayDuration={delayDuration} open={open}>
<TooltipTrigger asChild>{children}</TooltipTrigger> <TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent <TooltipContent
className={cn(className)} className={cn(className)}