mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-13 02:24:44 +08:00
refactor: extract components folder
This commit is contained in:
28
web/src/components/deer-flow/fav-icon.tsx
Normal file
28
web/src/components/deer-flow/fav-icon.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
export function FavIcon({
|
||||
className,
|
||||
url,
|
||||
title,
|
||||
}: {
|
||||
className?: string;
|
||||
url: string;
|
||||
title?: string;
|
||||
}) {
|
||||
return (
|
||||
<img
|
||||
className={cn("bg-accent h-4 w-4 rounded-full shadow-sm", className)}
|
||||
width={16}
|
||||
height={16}
|
||||
src={new URL(url).origin + "/favicon.ico"}
|
||||
alt={title}
|
||||
onError={(e) => {
|
||||
e.currentTarget.src =
|
||||
"https://perishablepress.com/wp/wp-content/images/2021/favicon-standard.png";
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
web/src/components/deer-flow/icons/detective.tsx
Normal file
26
web/src/components/deer-flow/icons/detective.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
export function Detective({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
version="1.1"
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<g fill="currentcolor">
|
||||
<path
|
||||
d="M392.692,257.322c-1.172-8.125-2.488-16.98-3.807-25.984c-5.856-39.012-12.59-81.688-14.86-87.832
|
||||
c-4.318-11.715-18.371-44.723-68.217-25.984c-15.738,5.926-18.812,11.93-41.648,8.93c-17.273-2.27-28.326-15.59-52.336-24.668
|
||||
c-49.844-18.883-71.584,11.711-75.902,23.422c-2.27,6.148-9.004,67.121-14.86,106.133c-1.39,8.86-2.633,17.566-3.804,25.621
|
||||
c37.256,7.535,84.174,12.879,138.705,12.879C309.541,269.837,355.801,264.716,392.692,257.322z"
|
||||
/>
|
||||
<path
|
||||
d="M443.707,306.509c-8.051-2.196-16.834-4.246-26.057-6.148c-1.83-0.805-3.66-1.535-5.49-2.27h-0.072
|
||||
c-46.918,10.394-102.254,15.664-156.125,15.664c-53.652,0-108.768-5.27-155.541-15.516c-1.316,0.512-2.707,1.098-4.098,1.684
|
||||
c-8.858,1.828-17.348,3.73-25.106,5.781l-0.148,0.074C27.008,317.49,0,333.372,0,350.939c0,36.012,114.549,65.289,256.035,65.289
|
||||
c141.34,0,255.965-29.278,255.965-65.289C512,333.74,486.016,318.22,443.707,306.509z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
68
web/src/components/deer-flow/image.tsx
Normal file
68
web/src/components/deer-flow/image.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { memo, useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import { Tooltip } from "./tooltip";
|
||||
|
||||
function Image({
|
||||
className,
|
||||
imageClassName,
|
||||
imageTransition,
|
||||
src,
|
||||
alt,
|
||||
fallback = null,
|
||||
}: {
|
||||
className?: string;
|
||||
imageClassName?: string;
|
||||
imageTransition?: boolean;
|
||||
src: string;
|
||||
alt: string;
|
||||
fallback?: React.ReactNode;
|
||||
}) {
|
||||
const [, 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 title={alt ?? "No caption"}>
|
||||
<img
|
||||
className={cn(
|
||||
"size-full object-cover",
|
||||
imageTransition && "transition-all duration-200 ease-out",
|
||||
imageClassName,
|
||||
)}
|
||||
src={src}
|
||||
alt={alt}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Image);
|
||||
34
web/src/components/deer-flow/loading-animation.module.css
Normal file
34
web/src/components/deer-flow/loading-animation.module.css
Normal file
@@ -0,0 +1,34 @@
|
||||
@keyframes bouncing-animation {
|
||||
to {
|
||||
opacity: 0.1;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
|
||||
.loadingAnimation {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.loadingAnimation > div {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin: 2px 4px;
|
||||
border-radius: 50%;
|
||||
background-color: #a3a1a1;
|
||||
opacity: 1;
|
||||
animation: bouncing-animation 0.5s infinite alternate;
|
||||
}
|
||||
|
||||
.loadingAnimation.sm > div {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin: 1px 2px;
|
||||
}
|
||||
|
||||
.loadingAnimation > div:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.loadingAnimation > div:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
28
web/src/components/deer-flow/loading-animation.tsx
Normal file
28
web/src/components/deer-flow/loading-animation.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import styles from "./loading-animation.module.css";
|
||||
|
||||
export function LoadingAnimation({
|
||||
className,
|
||||
size = "normal",
|
||||
}: {
|
||||
className?: string;
|
||||
size?: "normal" | "sm";
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
styles.loadingAnimation,
|
||||
size === "sm" && styles.sm,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
web/src/components/deer-flow/logo.tsx
Normal file
15
web/src/components/deer-flow/logo.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
export function Logo() {
|
||||
return (
|
||||
<Link
|
||||
className="opacity-70 transition-opacity duration-300 hover:opacity-100"
|
||||
href="/"
|
||||
>
|
||||
🦌 DeerFlow
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
128
web/src/components/deer-flow/markdown.tsx
Normal file
128
web/src/components/deer-flow/markdown.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import ReactMarkdown, {
|
||||
type Options as ReactMarkdownOptions,
|
||||
} from "react-markdown";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { rehypeSplitWordsIntoSpans } from "~/core/rehype";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import Image from "./image";
|
||||
import { Tooltip } from "./tooltip";
|
||||
|
||||
export function Markdown({
|
||||
className,
|
||||
children,
|
||||
style,
|
||||
enableCopy,
|
||||
animate = false,
|
||||
...props
|
||||
}: ReactMarkdownOptions & {
|
||||
className?: string;
|
||||
enableCopy?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
animate?: boolean;
|
||||
}) {
|
||||
const rehypePlugins = useMemo(() => {
|
||||
if (animate) {
|
||||
return [rehypeKatex, rehypeSplitWordsIntoSpans];
|
||||
}
|
||||
return [rehypeKatex];
|
||||
}, [animate]);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
"prose dark:prose-invert prose-p:my-0 prose-img:mt-0 flex flex-col gap-4",
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={{
|
||||
a: ({ href, children }) => (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
img: ({ src, alt }) => (
|
||||
<a href={src as string} target="_blank" rel="noopener noreferrer">
|
||||
<Image className="rounded" src={src as string} alt={alt ?? ""} />
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{dropMarkdownQuote(processKatexInMarkdown(children))}
|
||||
</ReactMarkdown>
|
||||
{enableCopy && typeof children === "string" && (
|
||||
<div className="flex">
|
||||
<CopyButton content={children} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyButton({ content }: { content: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
return (
|
||||
<Tooltip title="Copy">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-full"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}{" "}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function processKatexInMarkdown(markdown?: string | null) {
|
||||
if (!markdown) return markdown;
|
||||
|
||||
const markdownWithKatexSyntax = markdown
|
||||
.replace(/\\\\\[/g, "$$$$") // Replace '\\[' with '$$'
|
||||
.replace(/\\\\\]/g, "$$$$") // Replace '\\]' with '$$'
|
||||
.replace(/\\\\\(/g, "$$$$") // Replace '\\(' with '$$'
|
||||
.replace(/\\\\\)/g, "$$$$") // Replace '\\)' with '$$'
|
||||
.replace(/\\\[/g, "$$$$") // Replace '\[' with '$$'
|
||||
.replace(/\\\]/g, "$$$$") // Replace '\]' with '$$'
|
||||
.replace(/\\\(/g, "$$$$") // Replace '\(' with '$$'
|
||||
.replace(/\\\)/g, "$$$$"); // Replace '\)' with '$$';
|
||||
return markdownWithKatexSyntax;
|
||||
}
|
||||
|
||||
function dropMarkdownQuote(markdown?: string | null) {
|
||||
if (!markdown) return markdown;
|
||||
return markdown
|
||||
.replace(/^```markdown\n/gm, "")
|
||||
.replace(/^```text\n/gm, "")
|
||||
.replace(/^```\n/gm, "")
|
||||
.replace(/\n```$/gm, "");
|
||||
}
|
||||
24
web/src/components/deer-flow/rainbow-text.module.css
Normal file
24
web/src/components/deer-flow/rainbow-text.module.css
Normal file
@@ -0,0 +1,24 @@
|
||||
.animated {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgb(from var(--card-foreground) r g b / 0.3) 15%,
|
||||
rgb(from var(--card-foreground) r g b / 0.75) 35%,
|
||||
rgb(from var(--card-foreground) r g b / 0.75) 65%,
|
||||
rgb(from var(--card-foreground) r g b / 0.3) 85%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-fill-color: transparent;
|
||||
background-size: 500% auto;
|
||||
animation: textShine 2s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes textShine {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
22
web/src/components/deer-flow/rainbow-text.tsx
Normal file
22
web/src/components/deer-flow/rainbow-text.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import styles from "./rainbow-text.module.css";
|
||||
|
||||
export function RainbowText({
|
||||
animated,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
animated?: boolean;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<span className={cn(animated && styles.animated, className)}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
47
web/src/components/deer-flow/ray.tsx
Normal file
47
web/src/components/deer-flow/ray.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
export function Ray() {
|
||||
return (
|
||||
<svg
|
||||
className="animate-spotlight pointer-events-none fixed -top-80 left-0 z-[99] h-[169%] w-[138%] opacity-0 md:-top-20 md:left-60 lg:w-[84%]"
|
||||
viewBox="0 0 3787 2842"
|
||||
fill="none"
|
||||
>
|
||||
<g filter="url(#filter)">
|
||||
<ellipse
|
||||
cx="1924.71"
|
||||
cy="273.501"
|
||||
rx="1924.71"
|
||||
ry="273.501"
|
||||
transform="matrix(-0.822377 -0.568943 -0.568943 0.822377 3631.88 2291.09)"
|
||||
fill="white"
|
||||
fillOpacity="0.21"
|
||||
></ellipse>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter"
|
||||
x="0.860352"
|
||||
y="0.838989"
|
||||
width="3785.16"
|
||||
height="2840.26"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix"></feFlood>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
></feBlend>
|
||||
<feGaussianBlur
|
||||
stdDeviation="151"
|
||||
result="effect1_foregroundBlur_1065_8"
|
||||
></feGaussianBlur>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
36
web/src/components/deer-flow/rolling-text.tsx
Normal file
36
web/src/components/deer-flow/rolling-text.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
export function RollingText({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string;
|
||||
children?: string | string[];
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"relative flex h-[2em] items-center overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
<motion.div
|
||||
className="absolute w-fit"
|
||||
style={{ transition: "all 0.3s ease-in-out" }}
|
||||
initial={{ y: "100%", opacity: 0 }}
|
||||
animate={{ y: "0%", opacity: 1 }}
|
||||
exit={{ y: "-100%", opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
76
web/src/components/deer-flow/scroll-container.tsx
Normal file
76
web/src/components/deer-flow/scroll-container.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useStickToBottom } from "use-stick-to-bottom";
|
||||
|
||||
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
export function ScrollContainer({
|
||||
className,
|
||||
children,
|
||||
scrollShadow = true,
|
||||
scrollShadowColor = "var(--background)",
|
||||
autoScrollToBottom = false,
|
||||
}: {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
scrollShadow?: boolean;
|
||||
scrollShadowColor?: string;
|
||||
autoScrollToBottom?: boolean;
|
||||
}) {
|
||||
const { scrollRef, contentRef } = useStickToBottom({
|
||||
initial: "instant",
|
||||
});
|
||||
const tempScrollRef = useRef<HTMLElement>(null);
|
||||
const tempContentRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoScrollToBottom) {
|
||||
tempScrollRef.current = scrollRef.current;
|
||||
tempContentRef.current = contentRef.current;
|
||||
scrollRef.current = null;
|
||||
contentRef.current = null;
|
||||
} else if (tempScrollRef.current && tempContentRef.current) {
|
||||
scrollRef.current = tempScrollRef.current;
|
||||
contentRef.current = tempContentRef.current;
|
||||
}
|
||||
}, [autoScrollToBottom, contentRef, scrollRef]);
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)}>
|
||||
{scrollShadow && (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-0 right-0 left-0 z-10 h-10 bg-gradient-to-t",
|
||||
`from-transparent to-[var(--scroll-shadow-color)]`,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--scroll-shadow-color": scrollShadowColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-0 bottom-0 left-0 z-10 h-10 bg-gradient-to-b",
|
||||
`from-transparent to-[var(--scroll-shadow-color)]`,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--scroll-shadow-color": scrollShadowColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
></div>
|
||||
</>
|
||||
)}
|
||||
<ScrollArea ref={scrollRef} className="h-full w-full">
|
||||
<div className="h-fit w-full" ref={contentRef}>
|
||||
{children}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
web/src/components/deer-flow/theme-provider-wrapper.tsx
Normal file
29
web/src/components/deer-flow/theme-provider-wrapper.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { ThemeProvider } from "~/components/theme-provider";
|
||||
|
||||
export function ThemeProviderWrapper({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const isChatPage = pathname?.startsWith("/chat");
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme={"dark"}
|
||||
enableSystem={isChatPage}
|
||||
forcedTheme={isChatPage ? undefined : "dark"}
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
67
web/src/components/deer-flow/theme-toggle.tsx
Normal file
67
web/src/components/deer-flow/theme-toggle.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
"use client";
|
||||
|
||||
import { Monitor, Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import { Tooltip } from "./tooltip";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme = "system", setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<Tooltip title="Change theme">
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
<Sun className="mr-2 h-4 w-4" />
|
||||
<span
|
||||
className={cn(
|
||||
theme === "light" ? "font-bold" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
Light
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
<Moon className="mr-2 h-4 w-4" />
|
||||
<span
|
||||
className={cn(
|
||||
theme === "dark" ? "font-bold" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
Dark
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
<Monitor className="mr-2 h-4 w-4" />
|
||||
<span
|
||||
className={cn(
|
||||
theme === "system" ? "font-bold" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
System
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
33
web/src/components/deer-flow/toaster.tsx
Normal file
33
web/src/components/deer-flow/toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner } from "sonner";
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { resolvedTheme = "dark" } = useTheme();
|
||||
return (
|
||||
<Sonner
|
||||
theme={resolvedTheme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
43
web/src/components/deer-flow/tooltip.tsx
Normal file
43
web/src/components/deer-flow/tooltip.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import {
|
||||
Tooltip as ShadcnTooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "~/components/ui/tooltip";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
export function Tooltip({
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
title,
|
||||
open,
|
||||
side,
|
||||
sideOffset,
|
||||
}: {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
open?: boolean;
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
sideOffset?: number;
|
||||
}) {
|
||||
return (
|
||||
<ShadcnTooltip delayDuration={750} open={open}>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className={cn(className)}
|
||||
style={style}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
>
|
||||
{title}
|
||||
</TooltipContent>
|
||||
</ShadcnTooltip>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user