mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-13 18:24:45 +08:00
feat: support static website
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 "computer", which can execute commands,
|
||||
manage files, and run long tasks — all in a secure Docker-based
|
||||
sandbox
|
||||
</p>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -47,7 +47,6 @@ export function SettingsDialog({
|
||||
t.settings.sections.skills,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog {...dialogProps}>
|
||||
<DialogContent
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user