mirror of
https://git.mirrors.martin98.com/https://github.com/bytedance/deer-flow
synced 2025-08-18 05:55:56 +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 { cn } from "~/lib/utils";
|
||||
|
||||
import Image from "./image";
|
||||
|
||||
export function Markdown({
|
||||
className,
|
||||
children,
|
||||
@ -39,10 +41,6 @@ export function Markdown({
|
||||
}
|
||||
return [rehypeKatex];
|
||||
}, [animate]);
|
||||
const handleImgError = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
e.currentTarget.style.display = "none";
|
||||
console.warn(`Markdown: Image "${e.currentTarget.src}" failed to load`);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={cn(className, "markdown flex flex-col gap-4")}
|
||||
@ -58,7 +56,9 @@ export function Markdown({
|
||||
</a>
|
||||
),
|
||||
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}
|
||||
|
@ -18,6 +18,7 @@ import { useMessage, useStore } from "~/core/store";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import { FavIcon } from "./fav-icon";
|
||||
import Image from "./image";
|
||||
import { LoadingAnimation } from "./loading-animation";
|
||||
import { Markdown } from "./markdown";
|
||||
import { RainbowText } from "./rainbow-text";
|
||||
@ -123,7 +124,6 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
} else {
|
||||
results = [];
|
||||
}
|
||||
console.info(results);
|
||||
return results;
|
||||
}, [toolCall.result]);
|
||||
const pageResults = useMemo(
|
||||
@ -172,20 +172,30 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
</motion.li>
|
||||
))}
|
||||
{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
|
||||
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}
|
||||
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"
|
||||
style={{
|
||||
backgroundImage: `url(${searchResult.image_url})`,
|
||||
}}
|
||||
imageClassName="hover:scale-110"
|
||||
imageTransition
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
Loading…
x
Reference in New Issue
Block a user