mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-21 13:24:44 +08:00
feat: support dark mode
This commit is contained in:
@@ -41,7 +41,7 @@ export function ConversationStarter({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer rounded-2xl border bg-[rgba(255,255,255,0.5)] px-4 py-4 text-gray-500 transition-all duration-300 hover:bg-[rgba(255,255,255,1)] hover:text-gray-900 hover:shadow-md"
|
||||
className="bg-card text-muted-foreground cursor-pointer rounded-2xl border px-4 py-4 opacity-75 transition-all duration-300 hover:opacity-100 hover:shadow-md"
|
||||
onClick={() => {
|
||||
onSend?.(question);
|
||||
}}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
export function FavIcon({ url, title }: { url: string; title?: string }) {
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
export function FavIcon({
|
||||
className,
|
||||
url,
|
||||
title,
|
||||
}: {
|
||||
className?: string;
|
||||
url: string;
|
||||
title?: string;
|
||||
}) {
|
||||
return (
|
||||
<img
|
||||
className="h-4 w-4 rounded-full bg-slate-100 shadow-sm"
|
||||
className={cn("bg-accent h-4 w-4 rounded-full shadow-sm", className)}
|
||||
width={16}
|
||||
height={16}
|
||||
src={new URL(url).origin + "/favicon.ico"}
|
||||
|
||||
@@ -93,23 +93,23 @@ export function InputBox({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("relative rounded-[24px] border bg-white", className)}>
|
||||
<div className={cn("bg-card relative rounded-[24px] border", className)}>
|
||||
<div className="w-full">
|
||||
<AnimatePresence>
|
||||
{feedback && (
|
||||
<motion.div
|
||||
ref={feedbackRef}
|
||||
className="absolute top-0 left-0 mt-3 ml-2 flex items-center justify-center gap-1 rounded-2xl border border-[#007aff] bg-white px-2 py-0.5"
|
||||
className="bg-background border-brand absolute top-0 left-0 mt-3 ml-2 flex items-center justify-center gap-1 rounded-2xl border px-2 py-0.5"
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0 }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-center text-sm text-[#007aff] opacity-90">
|
||||
<div className="text-brand flex h-full w-full items-center justify-center text-sm opacity-90">
|
||||
{feedback.option.text}
|
||||
</div>
|
||||
<CloseOutlined
|
||||
className="cursor-pointer text-[9px]"
|
||||
className="cursor-pointer text-[9px] opacity-60"
|
||||
onClick={onRemoveFeedback}
|
||||
/>
|
||||
</motion.div>
|
||||
@@ -144,15 +144,12 @@ export function InputBox({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full",
|
||||
responding ? "bg-button-hover" : "bg-button",
|
||||
)}
|
||||
className={cn("h-10 w-10 rounded-full")}
|
||||
onClick={handleSendMessage}
|
||||
>
|
||||
{responding ? (
|
||||
<div className="flex h-10 w-10 items-center justify-center">
|
||||
<div className="h-4 w-4 rounded-sm bg-red-300" />
|
||||
<div className="bg-foreground h-4 w-4 rounded-sm opacity-70" />
|
||||
</div>
|
||||
) : (
|
||||
<ArrowUpOutlined />
|
||||
|
||||
@@ -81,7 +81,7 @@ export function MessageListView({
|
||||
"flex h-full w-full flex-col overflow-hidden pt-4",
|
||||
className,
|
||||
)}
|
||||
scrollShadowColor="#f7f5f3"
|
||||
scrollShadowColor="var(--app-background)"
|
||||
>
|
||||
<ul className="flex flex-col">
|
||||
{messageIds.map((messageId) => (
|
||||
@@ -205,10 +205,10 @@ function MessageListItem({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
`flex w-fit max-w-[85%] flex-col rounded-2xl px-4 py-3 shadow-xs`,
|
||||
`flex w-fit max-w-[85%] flex-col rounded-2xl px-4 py-3 shadow`,
|
||||
message.role === "user" &&
|
||||
"text-primary-foreground rounded-ee-none bg-[#007aff]",
|
||||
message.role === "assistant" && "rounded-es-none bg-white",
|
||||
"text-primary-foreground bg-brand rounded-ee-none",
|
||||
message.role === "assistant" && "bg-card rounded-es-none",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -249,7 +249,7 @@ function MessageListItem({
|
||||
}
|
||||
}, [openResearchId, researchId]);
|
||||
return (
|
||||
<Card className={cn("w-full bg-white", className)}>
|
||||
<Card className={cn("w-full", className)}>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<RainbowText animated={state !== "Report generated"}>
|
||||
@@ -259,7 +259,7 @@ function MessageListItem({
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<div className="flex w-full">
|
||||
<RollingText className="flex-grow text-sm opacity-50">
|
||||
<RollingText className="text-muted-foreground flex-grow text-sm">
|
||||
{state}
|
||||
</RollingText>
|
||||
<Button onClick={handleOpen}>
|
||||
@@ -302,7 +302,7 @@ function PlanCard({
|
||||
);
|
||||
}, []);
|
||||
return (
|
||||
<Card className={cn("w-full bg-white", className)}>
|
||||
<Card className={cn("w-full", className)}>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<h1 className="text-xl font-medium">
|
||||
@@ -336,7 +336,7 @@ function PlanCard({
|
||||
<CardFooter className="flex justify-end">
|
||||
{!message.isStreaming && interruptMessage?.options?.length && (
|
||||
<motion.div
|
||||
className="flex gap-2"
|
||||
className="flex gap-4"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
@@ -383,7 +383,7 @@ function PodcastCard({
|
||||
}, [message.isStreaming]);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
return (
|
||||
<Card className={cn("w-[508px] bg-white", className)}>
|
||||
<Card className={cn("w-[508px]", className)}>
|
||||
<CardHeader>
|
||||
<div className="text-muted-foreground flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.animated {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(0, 0, 0, 0.3) 15%,
|
||||
rgba(0, 0, 0, 0.7) 35%,
|
||||
rgba(0, 0, 0, 0.7) 65%,
|
||||
rgba(0, 0, 0, 0.3) 85%
|
||||
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;
|
||||
|
||||
@@ -161,7 +161,10 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
key={`search-result-${i}`}
|
||||
className="flex h-40 w-40 gap-2 rounded-md text-sm"
|
||||
>
|
||||
<Skeleton className="h-full w-full rounded-md bg-gradient-to-tl from-slate-50 to-slate-200" />
|
||||
<Skeleton
|
||||
className="to-accent h-full w-full rounded-md bg-gradient-to-tl from-slate-300"
|
||||
style={{ animationDelay: `${i * 0.2}s` }}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
{pageResults
|
||||
@@ -169,7 +172,7 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
.map((searchResult, i) => (
|
||||
<motion.li
|
||||
key={`search-result-${i}`}
|
||||
className="text-muted-foreground flex max-w-40 gap-2 rounded-md bg-slate-100 px-2 py-1 text-sm"
|
||||
className="text-muted-foreground bg-accent flex max-w-40 gap-2 rounded-md px-2 py-1 text-sm"
|
||||
initial={{ opacity: 0, y: 10, scale: 0.66 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
@@ -178,7 +181,11 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
<FavIcon url={searchResult.url} title={searchResult.title} />
|
||||
<FavIcon
|
||||
className="mt-1"
|
||||
url={searchResult.url}
|
||||
title={searchResult.title}
|
||||
/>
|
||||
<a href={searchResult.url} target="_blank">
|
||||
{searchResult.title}
|
||||
</a>
|
||||
@@ -203,7 +210,7 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
<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="bg-accent h-40 w-40 max-w-full rounded-md bg-cover bg-center bg-no-repeat"
|
||||
imageClassName="hover:scale-110"
|
||||
imageTransition
|
||||
/>
|
||||
@@ -237,7 +244,7 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
<div className="px-5">
|
||||
<ul className="mt-2 flex flex-wrap gap-4">
|
||||
<motion.li
|
||||
className="text-muted-foreground flex h-40 w-40 gap-2 rounded-md bg-slate-100 px-2 py-1 text-sm"
|
||||
className="text-muted-foreground bg-accent flex h-40 w-40 gap-2 rounded-md px-2 py-1 text-sm"
|
||||
initial={{ opacity: 0, y: 10, scale: 0.66 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
@@ -245,7 +252,7 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
<FavIcon url={url} title={title} />
|
||||
<FavIcon className="mt-1" url={url} title={title} />
|
||||
<a href={url} target="_blank">
|
||||
{title}
|
||||
</a>
|
||||
@@ -269,7 +276,7 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
</RainbowText>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<div className="mt-2 rounded-md bg-slate-50 p-2 text-sm">
|
||||
<div className="bg-accent mt-2 rounded-md p-2 text-sm">
|
||||
<SyntaxHighlighter language="python" style={docco}>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
|
||||
@@ -79,7 +79,10 @@ export function ResearchBlock({
|
||||
</TabsList>
|
||||
</div>
|
||||
<TabsContent className="h-full min-h-0 flex-grow px-8" value="report">
|
||||
<ScrollContainer className="px-5pb-20 h-full">
|
||||
<ScrollContainer
|
||||
className="px-5pb-20 h-full"
|
||||
scrollShadowColor="var(--card)"
|
||||
>
|
||||
{reportId && researchId && (
|
||||
<ResearchReportBlock
|
||||
className="mt-4"
|
||||
@@ -93,7 +96,7 @@ export function ResearchBlock({
|
||||
className="h-full min-h-0 flex-grow px-8"
|
||||
value="activities"
|
||||
>
|
||||
<ScrollContainer className="h-full">
|
||||
<ScrollContainer className="h-full" scrollShadowColor="var(--card)">
|
||||
{researchId && (
|
||||
<ResearchActivitiesBlock
|
||||
className="mt-4"
|
||||
|
||||
@@ -10,7 +10,7 @@ export function ScrollContainer({
|
||||
className,
|
||||
children,
|
||||
scrollShadow = true,
|
||||
scrollShadowColor = "white",
|
||||
scrollShadowColor = "var(--background)",
|
||||
}: {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
|
||||
36
web/src/app/_components/theme-toggle.tsx
Normal file
36
web/src/app/_components/theme-toggle.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { 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";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<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>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user