mirror of
https://git.mirrors.martin98.com/https://github.com/bytedance/deer-flow
synced 2025-08-19 15:09:10 +08:00
Check the output links are hallucinations from AI (#139)
* feat: check output links if a hallucination from AI
This commit is contained in:
parent
25e7b86f02
commit
bf4820c68f
@ -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>
|
||||||
|
@ -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}
|
||||||
|
50
web/src/components/deer-flow/link.tsx
Normal file
50
web/src/components/deer-flow/link.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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}
|
||||||
|
@ -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();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user