feat: support static website

This commit is contained in:
Henry Li
2026-01-24 18:01:27 +08:00
parent c66995bcc0
commit ebda30c7cf
36 changed files with 4889 additions and 92 deletions

View File

@@ -438,6 +438,7 @@ export type PromptInputProps = Omit<
"onSubmit" | "onError"
> & {
accept?: string; // e.g., "image/*" or leave undefined for any
disabled?: boolean;
multiple?: boolean;
// When true, accepts drops anywhere on document. Default false (opt-in).
globalDrop?: boolean;
@@ -459,6 +460,7 @@ export type PromptInputProps = Omit<
export const PromptInput = ({
className,
accept,
disabled,
multiple,
globalDrop,
syncHiddenInput,

View File

@@ -28,7 +28,7 @@ export function Hero({ className }: { className?: string }) {
/>
</div>
<FlickeringGrid
className="absolute inset-0 z-0 mask-[url(/images/deer.svg)] mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
className="absolute inset-0 z-0 translate-y-8 mask-[url(/images/deer.svg)] mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
squareSize={4}
gridGap={4}
color={"white"}

View File

@@ -9,10 +9,13 @@ import {
Sparkles,
Terminal,
Play,
Pause,
} from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { useState, useEffect, useRef } from "react";
import { Tooltip } from "@/components/workspace/tooltip";
type AnimationPhase =
| "idle"
| "user-input"
@@ -69,13 +72,19 @@ export default function ProgressiveSkillsAnimation() {
const [hasAutoPlayed, setHasAutoPlayed] = useState(false);
const chatMessagesRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const timeoutsRef = useRef<NodeJS.Timeout[]>([]);
// Additional display duration after the final step (done) completes, used to show the final result
const FINAL_DISPLAY_DURATION = 3000; // milliseconds
// Play animation only when isPlaying is true
useEffect(() => {
if (!isPlaying) return;
if (!isPlaying) {
// Clear all timeouts when paused
timeoutsRef.current.forEach(clearTimeout);
timeoutsRef.current = [];
return;
}
const timeline = [
{ phase: "user-input" as const, delay: ANIMATION_DELAYS["user-input"] },
@@ -117,7 +126,12 @@ export default function ProgressiveSkillsAnimation() {
}, totalDelay + FINAL_DISPLAY_DURATION),
);
return () => timeouts.forEach(clearTimeout);
timeoutsRef.current = timeouts;
return () => {
timeouts.forEach(clearTimeout);
timeoutsRef.current = [];
};
}, [isPlaying]);
const handlePlay = () => {
@@ -130,6 +144,20 @@ export default function ProgressiveSkillsAnimation() {
setShowWorkspace(false);
};
const handleTogglePlayPause = () => {
if (isPlaying) {
setIsPlaying(false);
} else {
// If animation hasn't started or is at idle, restart from beginning
if (phase === "idle") {
handlePlay();
} else {
// Resume from current phase
setIsPlaying(true);
}
}
};
// Auto-play when component enters viewport for the first time
useEffect(() => {
if (hasAutoPlayed || !containerRef.current) return;
@@ -308,7 +336,7 @@ export default function ProgressiveSkillsAnimation() {
>
{/* Overlay and Play Button */}
<AnimatePresence>
{!isPlaying && (
{!isPlaying && !hasPlayed && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -330,13 +358,30 @@ export default function ProgressiveSkillsAnimation() {
/>
</div>
<span className="text-lg font-medium text-white">
{hasPlayed ? "Click to replay" : "Click to play"}
Click to play
</span>
</motion.button>
</motion.div>
)}
</AnimatePresence>
{/* Bottom Left Play/Pause Button */}
<Tooltip content="Play / Pause">
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
onClick={handleTogglePlayPause}
className="absolute bottom-8 left-8 z-40 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 backdrop-blur-md transition-all hover:scale-110 hover:bg-white/20 active:scale-95"
aria-label={isPlaying ? "暂停" : "播放"}
>
{isPlaying ? (
<Pause size={24} className="text-white" fill="white" />
) : (
<Play size={24} className="ml-0.5 text-white" fill="white" />
)}
</motion.button>
</Tooltip>
<div className="flex h-full max-h-[700px] w-full max-w-6xl gap-8">
{/* Left: File Tree */}
<div className="flex flex-1 flex-col">
@@ -588,7 +633,7 @@ export default function ProgressiveSkillsAnimation() {
className="flex items-center gap-2 text-sm text-green-500"
>
<FileText size={14} />
<span>{file}</span>
<span>Generating {file}...</span>
<Check size={14} />
</motion.div>
))}
@@ -617,7 +662,7 @@ export default function ProgressiveSkillsAnimation() {
className="flex items-center gap-2 pl-4 text-zinc-400"
>
<Terminal size={16} />
<span>Executing deploy.sh</span>
<span>Executing scripts/deploy.sh</span>
</motion.div>
)}
</div>

View File

@@ -12,11 +12,12 @@ export function SandboxSection({ className }: { className?: string }) {
return (
<Section
className={className}
title="Sandbox"
title="Agent Runtime Environment"
subtitle={
<p>
We gave DeerFlow a computer. It can execute code, manage files, and
run long tasks all in a secure Docker sandbox
We give DeerFlow a &quot;computer&quot;, which can execute commands,
manage files, and run long tasks all in a secure Docker-based
sandbox
</p>
}
>

View File

@@ -2,13 +2,13 @@
import { cn } from "@/lib/utils";
import ProgressiveSkillsAnimation from "../components/progressive-skills-animation";
import ProgressiveSkillsAnimation from "../progressive-skills-animation";
import { Section } from "../section";
export function SkillsSection({ className }: { className?: string }) {
return (
<Section
className={cn("h-[calc(100vh-64px)] w-full bg-white/7", className)}
className={cn("h-[calc(100vh-64px)] w-full bg-white/2", className)}
title="Skill-based Architecture"
subtitle={
<div>

View File

@@ -1,10 +1,57 @@
"use client";
import MagicBento from "@/components/ui/magic-bento";
import MagicBento, { type BentoCardProps } from "@/components/ui/magic-bento";
import { cn } from "@/lib/utils";
import { Section } from "../section";
const COLOR = "#0a0a0a";
const features: BentoCardProps[] = [
{
color: COLOR,
label: "Context Engineering",
title: "Long/Short-term Memory",
description: (
<div>
<div>Now the agent can better understand you</div>
<div className="text-muted-foreground">Coming soon</div>
</div>
),
},
{
color: COLOR,
label: "Long Task Running",
title: "Planning and Reasoning",
description: "Plans ahead, reasons through complexity, then acts",
},
{
color: COLOR,
label: "Extensible",
title: "Skills and Tools",
description:
"Plug, play, or even swap built-in tools. Build the agent you want.",
},
{
color: COLOR,
label: "Persistent",
title: "Sandbox with File System",
description: "Read, write, run — like a real computer",
},
{
color: COLOR,
label: "Flexible",
title: "Multi-Model Support",
description: "Doubao, DeepSeek, OpenAI, Gemini, etc.",
},
{
color: COLOR,
label: "Free",
title: "Open Source",
description: "MIT License, self-hosted, full control",
},
];
export function WhatsNewSection({ className }: { className?: string }) {
return (
<Section
@@ -13,7 +60,7 @@ export function WhatsNewSection({ className }: { className?: string }) {
subtitle="DeerFlow is now evolving from a Deep Research agent into a full-stack Super Agent"
>
<div className="flex w-full items-center justify-center">
<MagicBento />
<MagicBento data={features} />
</div>
</Section>
);

View File

@@ -23,6 +23,7 @@ export interface BentoProps {
glowColor?: string;
clickEffect?: boolean;
enableMagnetism?: boolean;
data: BentoCardProps[];
}
const DEFAULT_PARTICLE_COUNT = 12;
@@ -30,52 +31,6 @@ const DEFAULT_SPOTLIGHT_RADIUS = 300;
const DEFAULT_GLOW_COLOR = "132, 0, 255";
const MOBILE_BREAKPOINT = 768;
const cardData: BentoCardProps[] = [
{
color: "#0a0015",
title: "Long/Short-term Memory",
description: (
<div>
<div>Now the agent can better understand you</div>
<div className="text-muted-foreground">Coming soon</div>
</div>
),
label: "Context Engineering",
},
{
color: "#0a0015",
title: "Planning and Reasoning",
description: "Plans ahead, reasons through complexity, then acts",
label: "Long Task Running",
},
{
color: "#0a0015",
title: "Skills and Tools",
description:
"Plug, play, or even swap built-in tools. Build the agent you want.",
label: "Extensible",
},
{
color: "#0a0015",
title: "Sandbox with File System",
description: "Read, write, run — like a real computer",
label: "Persistent",
},
{
color: "#0a0015",
title: "Multi-Model Support",
description: "Doubao, DeepSeek, OpenAI, Gemini, etc.",
label: "Flexible",
},
{
color: "#0a0015",
title: "Open Source",
description: "MIT License, self-hosted, full control",
label: "Free",
},
];
const createParticleElement = (
x: number,
y: number,
@@ -571,6 +526,7 @@ const MagicBento: React.FC<BentoProps> = ({
glowColor = DEFAULT_GLOW_COLOR,
clickEffect = true,
enableMagnetism = true,
data: cardData,
}) => {
const gridRef = useRef<HTMLDivElement>(null);
const isMobile = useMobileDetection();

View File

@@ -230,7 +230,7 @@ export const Terminal = ({
<div
ref={containerRef}
className={cn(
"border-border bg-background z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border",
"border-border bg-background/25 z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border",
className,
)}
>

View File

@@ -48,6 +48,7 @@ import {
export function InputBox({
className,
disabled,
autoFocus,
status = "ready",
context,
@@ -60,6 +61,7 @@ export function InputBox({
}: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & {
assistantId?: string | null;
status?: ChatStatus;
disabled?: boolean;
context: Omit<AgentThreadContext, "thread_id">;
extraHeader?: React.ReactNode;
isNewThread?: boolean;
@@ -142,6 +144,7 @@ export function InputBox({
"bg-background/85 rounded-2xl backdrop-blur-sm transition-all duration-300 ease-out *:data-[slot='input-group']:rounded-2xl",
className,
)}
disabled={disabled}
globalDrop
multiple
onSubmit={handleSubmit}
@@ -160,6 +163,7 @@ export function InputBox({
<PromptInputBody className="absolute top-0 right-0 left-0 z-3">
<PromptInputTextarea
className={cn("size-full")}
disabled={disabled}
placeholder={t.inputBox.placeholder}
autoFocus={autoFocus}
/>
@@ -303,6 +307,7 @@ export function InputBox({
</ModelSelector>
<PromptInputSubmit
className="rounded-full"
disabled={disabled}
variant="outline"
status={status}
/>

View File

@@ -23,6 +23,7 @@ import {
import { useI18n } from "@/core/i18n/hooks";
import { useDeleteThread, useThreads } from "@/core/threads/hooks";
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
import { env } from "@/env";
export function RecentChatList() {
const { t } = useI18n();
@@ -54,7 +55,11 @@ export function RecentChatList() {
}
return (
<SidebarGroup>
<SidebarGroupLabel>{t.sidebar.recentChats}</SidebarGroupLabel>
<SidebarGroupLabel>
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true"
? t.sidebar.recentChats
: t.sidebar.demoChats}
</SidebarGroupLabel>
<SidebarGroupContent className="group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0">
<SidebarMenu>
<div className="flex w-full flex-col gap-1">
@@ -73,29 +78,31 @@ export function RecentChatList() {
>
{titleOfThread(thread)}
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction
showOnHover
className="bg-background/50 hover:bg-background"
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true" && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction
showOnHover
className="bg-background/50 hover:bg-background"
>
<MoreHorizontal />
<span className="sr-only">{t.common.more}</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={"right"}
align={"start"}
>
<MoreHorizontal />
<span className="sr-only">{t.common.more}</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={"right"}
align={"start"}
>
<DropdownMenuItem
onSelect={() => handleDelete(thread.thread_id)}
>
<Trash2 className="text-muted-foreground" />
<span>{t.common.delete}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenuItem
onSelect={() => handleDelete(thread.thread_id)}
>
<Trash2 className="text-muted-foreground" />
<span>{t.common.delete}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</SidebarMenuButton>
</SidebarMenuItem>

View File

@@ -47,7 +47,6 @@ export function SettingsDialog({
t.settings.sections.skills,
],
);
return (
<Dialog {...dialogProps}>
<DialogContent

View File

@@ -22,6 +22,7 @@ import { Switch } from "@/components/ui/switch";
import { useI18n } from "@/core/i18n/hooks";
import { useEnableSkill, useSkills } from "@/core/skills/hooks";
import type { Skill } from "@/core/skills/type";
import { env } from "@/env";
import { SettingsSection } from "./settings-section";
@@ -116,6 +117,7 @@ function SkillSettingsList({ skills }: { skills: Skill[] }) {
<ItemActions>
<Switch
checked={skill.enabled}
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
onCheckedChange={(checked) =>
enableSkill({ skillName: skill.name, enabled: checked })
}

View File

@@ -11,6 +11,7 @@ import { Switch } from "@/components/ui/switch";
import { useI18n } from "@/core/i18n/hooks";
import { useMCPConfig, useEnableMCPServer } from "@/core/mcp/hooks";
import type { MCPServerConfig } from "@/core/mcp/types";
import { env } from "@/env";
import { SettingsSection } from "./settings-section";
@@ -56,6 +57,7 @@ function MCPServerList({
<ItemActions>
<Switch
checked={config.enabled}
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
onCheckedChange={(checked) =>
enableMCPServer({ serverName: name, enabled: checked })
}

View File

@@ -12,7 +12,9 @@ import {
useSidebar,
} from "@/components/ui/sidebar";
import { useI18n } from "@/core/i18n/hooks";
import { env } from "@/env";
import { cn } from "@/lib/utils";
import { Tooltip } from "./tooltip";
export function WorkspaceHeader({ className }: { className?: string }) {
const { t } = useI18n();
@@ -35,7 +37,7 @@ export function WorkspaceHeader({ className }: { className?: string }) {
</div>
) : (
<div className="flex items-center justify-between gap-2">
<Link href="/workspace" className="text-primary ml-2 font-serif">
<Link href="/" className="text-primary ml-2 font-serif">
DeerFlow
</Link>
<SidebarTrigger />