Check the output links are hallucinations from AI (#139)

* feat: check output links if a hallucination from AI
This commit is contained in:
JeffJiang 2025-05-15 10:39:53 +08:00 committed by GitHub
parent 25e7b86f02
commit bf4820c68f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 88 additions and 14 deletions

View File

@ -1,7 +1,7 @@
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { Check, Copy, Headphones, X } from "lucide-react"; import { Check, Copy, Headphones, Pencil, Undo2, X } from "lucide-react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { ScrollContainer } from "~/components/deer-flow/scroll-container"; import { ScrollContainer } from "~/components/deer-flow/scroll-container";
@ -47,6 +47,7 @@ export function ResearchBlock({
await listenToPodcast(researchId); await listenToPodcast(researchId);
}, [researchId]); }, [researchId]);
const [editing, setEditing] = useState(false);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const handleCopy = useCallback(() => { const handleCopy = useCallback(() => {
if (!reportId) { if (!reportId) {
@ -63,6 +64,10 @@ export function ResearchBlock({
}, 1000); }, 1000);
}, [reportId]); }, [reportId]);
const handleEdit = useCallback(() => {
setEditing((editing) => !editing);
}, []);
// When the research id changes, set the active tab to activities // When the research id changes, set the active tab to activities
useEffect(() => { useEffect(() => {
if (!hasReport) { if (!hasReport) {
@ -87,6 +92,16 @@ export function ResearchBlock({
<Headphones /> <Headphones />
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip title="Edit">
<Button
className="text-gray-400"
size="icon"
variant="ghost"
onClick={handleEdit}
>
{editing ? <Undo2 /> : <Pencil />}
</Button>
</Tooltip>
<Tooltip title="Copy"> <Tooltip title="Copy">
<Button <Button
className="text-gray-400" className="text-gray-400"
@ -147,6 +162,7 @@ export function ResearchBlock({
className="mt-4" className="mt-4"
researchId={researchId} researchId={researchId}
messageId={reportId} messageId={reportId}
editing={editing}
/> />
)} )}
</ScrollContainer> </ScrollContainer>

View File

@ -13,10 +13,12 @@ import { cn } from "~/lib/utils";
export function ResearchReportBlock({ export function ResearchReportBlock({
className, className,
messageId, messageId,
editing,
}: { }: {
className?: string; className?: string;
researchId: string; researchId: string;
messageId: string; messageId: string;
editing: boolean;
}) { }) {
const message = useMessage(messageId); const message = useMessage(messageId);
const { isReplay } = useReplay(); const { isReplay } = useReplay();
@ -55,7 +57,7 @@ export function ResearchReportBlock({
ref={contentRef} ref={contentRef}
className={cn("relative flex flex-col pt-4 pb-8", className)} className={cn("relative flex flex-col pt-4 pb-8", className)}
> >
{!isReplay && isCompleted ? ( {!isReplay && isCompleted && editing ? (
<ReportEditor <ReportEditor
content={message?.content} content={message?.content}
onMarkdownChange={handleMarkdownChange} onMarkdownChange={handleMarkdownChange}

View File

@ -0,0 +1,50 @@
import { useEffect, useMemo } from "react";
import { useToolCalls } from "~/core/store";
import { cn } from "~/lib/utils";
import { Tooltip } from "./tooltip";
export const Link = ({
href,
children,
}: {
href: string | undefined;
children: React.ReactNode;
}) => {
const toolCalls = useToolCalls();
const credibleLinks = useMemo(() => {
const links = new Set<string>();
(toolCalls || []).forEach((call) => {
if (call && call.name === "web_search" && call.result) {
const result = JSON.parse(call.result) as Array<{ url: string }>;
result.forEach((r) => {
links.add(r.url);
});
}
});
return links;
}, [toolCalls]);
const isCredible = useMemo(() => {
return href ? credibleLinks.has(href) : true;
}, [credibleLinks, href]);
if (isCredible) {
return (
<Tooltip title="This link might be a hallucination from AI model and may not be reliable.">
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={cn(isCredible && "after:ml-0.5 after:content-['⚠️']")}
>
{children}
</a>
</Tooltip>
);
}
return (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
);
};

View File

@ -18,13 +18,10 @@ import { cn } from "~/lib/utils";
import Image from "./image"; import Image from "./image";
import { Tooltip } from "./tooltip"; import { Tooltip } from "./tooltip";
import { Link } from "./link";
const components: ReactMarkdownOptions["components"] = { const components: ReactMarkdownOptions["components"] = {
a: ({ href, children }) => ( a: ({ href, children }) => <Link href={href}>{children}</Link>,
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
),
img: ({ src, alt }) => ( img: ({ src, alt }) => (
<a href={src as string} target="_blank" rel="noopener noreferrer"> <a href={src as string} target="_blank" rel="noopener noreferrer">
<Image className="rounded" src={src as string} alt={alt ?? ""} /> <Image className="rounded" src={src as string} alt={alt ?? ""} />
@ -52,13 +49,7 @@ export function Markdown({
return [rehypeKatex]; return [rehypeKatex];
}, [animated]); }, [animated]);
return ( return (
<div <div className={cn(className, "prose dark:prose-invert")} style={style}>
className={cn(
className,
"prose dark:prose-invert prose-p:my-0 prose-img:mt-0 flex flex-col gap-4",
)}
style={style}
>
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]} remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={rehypePlugins} rehypePlugins={rehypePlugins}

View File

@ -377,3 +377,14 @@ export function useLastFeedbackMessageId() {
); );
return waitingForFeedbackMessageId; return waitingForFeedbackMessageId;
} }
export function useToolCalls() {
return useStore(
useShallow((state) => {
return state.messageIds
?.map((id) => getMessage(id)?.toolCalls)
.filter((toolCalls) => toolCalls != null)
.flat();
}),
);
}

View File

@ -4,6 +4,10 @@
color: inherit; color: inherit;
} }
.ProseMirror {
line-height: 1.75;
}
.ProseMirror .is-editor-empty:first-child::before { .ProseMirror .is-editor-empty:first-child::before {
content: attr(data-placeholder); content: attr(data-placeholder);
float: left; float: left;