feat: support dark mode

This commit is contained in:
Li Xin
2025-04-20 11:18:05 +08:00
parent ce130e7160
commit a57db4fa4a
18 changed files with 952 additions and 53 deletions

View File

@@ -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);
}}

View File

@@ -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"}

View File

@@ -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 />

View File

@@ -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">

View File

@@ -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;

View File

@@ -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>

View File

@@ -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"

View File

@@ -10,7 +10,7 @@ export function ScrollContainer({
className,
children,
scrollShadow = true,
scrollShadowColor = "white",
scrollShadowColor = "var(--background)",
}: {
className?: string;
children: React.ReactNode;

View 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>
);
}