mirror of
https://git.mirrors.martin98.com/https://github.com/bytedance/deer-flow
synced 2025-08-19 01:55:59 +08:00
feat: add <Image/>
This commit is contained in:
parent
9758180e96
commit
d2478d9d4f
73
web/src/app/_components/image.tsx
Normal file
73
web/src/app/_components/image.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { memo, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "~/components/ui/tooltip";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
function Image({
|
||||||
|
className,
|
||||||
|
imageClassName,
|
||||||
|
imageTransition,
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
fallback = null,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
imageClassName?: string;
|
||||||
|
imageTransition?: boolean;
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
fallback?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsError(false);
|
||||||
|
setIsLoading(true);
|
||||||
|
}, [src]);
|
||||||
|
|
||||||
|
const handleLoad = useCallback(() => {
|
||||||
|
setIsError(false);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
const handleError = useCallback(
|
||||||
|
(e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||||
|
e.currentTarget.style.display = "none";
|
||||||
|
console.warn(`Markdown: Image "${e.currentTarget.src}" failed to load`);
|
||||||
|
setIsError(true);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<span className={cn("block w-fit overflow-hidden", className)}>
|
||||||
|
{isError ? (
|
||||||
|
fallback
|
||||||
|
) : (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<img
|
||||||
|
className={cn(
|
||||||
|
"size-full object-cover",
|
||||||
|
imageTransition && "transition-all duration-200 ease-out",
|
||||||
|
imageClassName,
|
||||||
|
)}
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
onLoad={handleLoad}
|
||||||
|
onError={handleError}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="max-w-64 text-sm">{alt ?? "No caption"}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Image);
|
@ -20,6 +20,8 @@ import {
|
|||||||
import { rehypeSplitWordsIntoSpans } from "~/core/rehype";
|
import { rehypeSplitWordsIntoSpans } from "~/core/rehype";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
import Image from "./image";
|
||||||
|
|
||||||
export function Markdown({
|
export function Markdown({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
@ -39,10 +41,6 @@ export function Markdown({
|
|||||||
}
|
}
|
||||||
return [rehypeKatex];
|
return [rehypeKatex];
|
||||||
}, [animate]);
|
}, [animate]);
|
||||||
const handleImgError = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
|
||||||
e.currentTarget.style.display = "none";
|
|
||||||
console.warn(`Markdown: Image "${e.currentTarget.src}" failed to load`);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(className, "markdown flex flex-col gap-4")}
|
className={cn(className, "markdown flex flex-col gap-4")}
|
||||||
@ -58,7 +56,9 @@ export function Markdown({
|
|||||||
</a>
|
</a>
|
||||||
),
|
),
|
||||||
img: ({ src, alt }) => (
|
img: ({ src, alt }) => (
|
||||||
<img src={src} alt={alt} onError={handleImgError} />
|
<a href={src as string} target="_blank" rel="noopener noreferrer">
|
||||||
|
<Image className="rounded" src={src as string} alt={alt ?? ""} />
|
||||||
|
</a>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -18,6 +18,7 @@ import { useMessage, useStore } from "~/core/store";
|
|||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
import { FavIcon } from "./fav-icon";
|
import { FavIcon } from "./fav-icon";
|
||||||
|
import Image from "./image";
|
||||||
import { LoadingAnimation } from "./loading-animation";
|
import { LoadingAnimation } from "./loading-animation";
|
||||||
import { Markdown } from "./markdown";
|
import { Markdown } from "./markdown";
|
||||||
import { RainbowText } from "./rainbow-text";
|
import { RainbowText } from "./rainbow-text";
|
||||||
@ -123,7 +124,6 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
} else {
|
} else {
|
||||||
results = [];
|
results = [];
|
||||||
}
|
}
|
||||||
console.info(results);
|
|
||||||
return results;
|
return results;
|
||||||
}, [toolCall.result]);
|
}, [toolCall.result]);
|
||||||
const pageResults = useMemo(
|
const pageResults = useMemo(
|
||||||
@ -172,20 +172,30 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
</motion.li>
|
</motion.li>
|
||||||
))}
|
))}
|
||||||
{imageResults.map((searchResult, i) => (
|
{imageResults.map((searchResult, i) => (
|
||||||
<li key={`search-result-${i}`}>
|
<motion.li
|
||||||
|
key={`search-result-${i}`}
|
||||||
|
initial={{ opacity: 0, y: 10, scale: 0.66 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.2,
|
||||||
|
delay: i * 0.1,
|
||||||
|
ease: "easeOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
className="flex flex-col gap-2 opacity-75 transition-opacity duration-300 hover:opacity-100"
|
className="flex flex-col gap-2 overflow-hidden rounded-md opacity-75 transition-opacity duration-300 hover:opacity-100"
|
||||||
href={searchResult.image_url}
|
href={searchResult.image_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div
|
<Image
|
||||||
|
src={searchResult.image_url}
|
||||||
|
alt={searchResult.image_description}
|
||||||
className="h-40 w-40 max-w-full rounded-md bg-slate-100 bg-cover bg-center bg-no-repeat"
|
className="h-40 w-40 max-w-full rounded-md bg-slate-100 bg-cover bg-center bg-no-repeat"
|
||||||
style={{
|
imageClassName="hover:scale-110"
|
||||||
backgroundImage: `url(${searchResult.image_url})`,
|
imageTransition
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</motion.li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user