mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-05 15:10:20 +08:00
feat: support static website
This commit is contained in:
26
frontend/src/app/mock/api/mcp/config/route.ts
Normal file
26
frontend/src/app/mock/api/mcp/config/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export function GET() {
|
||||
return Response.json({
|
||||
mcp_servers: {
|
||||
"mcp-github-trending": {
|
||||
enabled: true,
|
||||
type: "stdio",
|
||||
command: "uvx",
|
||||
args: ["mcp-github-trending"],
|
||||
env: {},
|
||||
url: null,
|
||||
headers: {},
|
||||
description:
|
||||
"A MCP server that provides access to GitHub trending repositories and developers data",
|
||||
},
|
||||
"context-7": {
|
||||
enabled: true,
|
||||
description:
|
||||
"Get the latest documentation and code into Cursor, Claude, or other LLMs",
|
||||
},
|
||||
"feishu-importer": {
|
||||
enabled: true,
|
||||
description: "Import Feishu documents",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
30
frontend/src/app/mock/api/models/route.ts
Normal file
30
frontend/src/app/mock/api/models/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export function GET() {
|
||||
return Response.json({
|
||||
models: [
|
||||
{
|
||||
id: "doubao-seed-1.8",
|
||||
name: "doubao-seed-1.8",
|
||||
display_name: "Doubao Seed 1.8",
|
||||
supports_thinking: true,
|
||||
},
|
||||
{
|
||||
id: "deepseek-v3.2",
|
||||
name: "deepseek-v3.2",
|
||||
display_name: "DeepSeek v3.2",
|
||||
supports_thinking: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-5",
|
||||
name: "gpt-5",
|
||||
display_name: "GPT-5",
|
||||
supports_thinking: true,
|
||||
},
|
||||
{
|
||||
id: "gemini-3-pro",
|
||||
name: "gemini-3-pro",
|
||||
display_name: "Gemini 3 Pro",
|
||||
supports_thinking: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
70
frontend/src/app/mock/api/skills/route.ts
Normal file
70
frontend/src/app/mock/api/skills/route.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export function GET() {
|
||||
return Response.json({
|
||||
skills: [
|
||||
{
|
||||
name: "frontend-design",
|
||||
description:
|
||||
"Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.",
|
||||
license: "Complete terms in LICENSE.txt",
|
||||
category: "public",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: "pdf-processing",
|
||||
description:
|
||||
"Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.",
|
||||
license: "Proprietary. LICENSE.txt has complete terms",
|
||||
category: "public",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: "vercel-deploy",
|
||||
description:
|
||||
'Deploy applications and websites to Vercel. Use this skill when the user requests deployment actions such as "Deploy my app", "Deploy this to production", "Create a preview deployment", "Deploy and give me the link", or "Push this live". No authentication required - returns preview URL and claimable deployment link.',
|
||||
license: null,
|
||||
category: "public",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: "web-design-guidelines",
|
||||
description:
|
||||
'Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".',
|
||||
license: null,
|
||||
category: "public",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: "cartoon-generator",
|
||||
description:
|
||||
'Generate cartoon images based on a description. Use when asked to "generate a cartoon image", "create a cartoon", "draw a cartoon", or "generate a cartoon image based on a description".',
|
||||
license: null,
|
||||
category: "custom",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: "podcast-generator",
|
||||
description:
|
||||
'Generate a podcast episode based on a topic. Use when asked to "generate a podcast episode", "create a podcast episode", "generate a podcast episode based on a topic", or "generate a podcast episode based on a description".',
|
||||
license: null,
|
||||
category: "custom",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: "advanced-data-analysis",
|
||||
description:
|
||||
'Perform advanced data analysis and visualization. Use when asked to "analyze data", "visualize data", "analyze data based on a description", or "visualize data based on a description".',
|
||||
license: null,
|
||||
category: "custom",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: "3d-model-generator",
|
||||
description:
|
||||
'Generate 3D models based on a description. Use when asked to "generate a 3D model", "create a 3D model", "generate a 3D model based on a description", or "generate a 3D model based on a description".',
|
||||
license: null,
|
||||
category: "custom",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
thread_id: string;
|
||||
artifact_path?: string[] | undefined;
|
||||
}>;
|
||||
},
|
||||
) {
|
||||
const threadId = (await params).thread_id;
|
||||
let artifactPath = (await params).artifact_path?.join("/") ?? "";
|
||||
if (artifactPath.startsWith("mnt/")) {
|
||||
artifactPath = path.resolve(
|
||||
process.cwd(),
|
||||
artifactPath.replace("mnt/", `public/demo/threads/${threadId}/`),
|
||||
);
|
||||
if (fs.existsSync(artifactPath)) {
|
||||
if (request.nextUrl.searchParams.get("download") === "true") {
|
||||
// Attach the file to the response
|
||||
const headers = new Headers();
|
||||
headers.set(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${artifactPath}"`,
|
||||
);
|
||||
return new Response(fs.readFileSync(artifactPath), {
|
||||
status: 200,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
return new Response(fs.readFileSync(artifactPath), { status: 200 });
|
||||
}
|
||||
}
|
||||
return new Response("File not found", { status: 404 });
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ thread_id: string }> },
|
||||
) {
|
||||
const threadId = (await params).thread_id;
|
||||
const jsonString = fs.readFileSync(
|
||||
path.resolve(process.cwd(), `public/demo/threads/${threadId}/thread.json`),
|
||||
"utf8",
|
||||
);
|
||||
const json = JSON.parse(jsonString);
|
||||
if (Array.isArray(json.history)) {
|
||||
return Response.json(json);
|
||||
}
|
||||
return Response.json([json]);
|
||||
}
|
||||
27
frontend/src/app/mock/api/threads/search/route.ts
Normal file
27
frontend/src/app/mock/api/threads/search/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export function POST() {
|
||||
const threadsDir = fs.readdirSync(
|
||||
path.resolve(process.cwd(), "public/demo/threads"),
|
||||
{
|
||||
withFileTypes: true,
|
||||
},
|
||||
);
|
||||
const threadData = threadsDir
|
||||
.map((threadId) => {
|
||||
if (threadId.isDirectory() && !threadId.name.startsWith(".")) {
|
||||
const threadData = fs.readFileSync(
|
||||
path.resolve(`public/demo/threads/${threadId.name}/thread.json`),
|
||||
"utf8",
|
||||
);
|
||||
return {
|
||||
thread_id: threadId.name,
|
||||
values: JSON.parse(threadData).values,
|
||||
};
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return Response.json(threadData);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { type AgentThread } from "@/core/threads";
|
||||
import { useSubmitThread, useThreadStream } from "@/core/threads/hooks";
|
||||
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
|
||||
import { uuid } from "@/core/utils/uuid";
|
||||
import { env } from "@/env";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function ChatPage() {
|
||||
@@ -176,12 +177,18 @@ export default function ChatPage() {
|
||||
status={thread.isLoading ? "streaming" : "ready"}
|
||||
context={settings.context}
|
||||
extraHeader={isNewThread && <Welcome />}
|
||||
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
|
||||
onContextChange={(context) =>
|
||||
setSettings("context", context)
|
||||
}
|
||||
onSubmit={handleSubmit}
|
||||
onStop={handleStop}
|
||||
/>
|
||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && (
|
||||
<div className="text-muted-foreground/67 w-full -translate-y-2 text-center text-xs">
|
||||
{t.common.notAvailableInDemoMode}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -51,9 +51,11 @@ export default function ChatsPage() {
|
||||
<div>
|
||||
<div>{titleOfThread(thread)}</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{formatTimeAgo(thread.updated_at)}
|
||||
</div>
|
||||
{thread.updated_at && (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{formatTimeAgo(thread.updated_at)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { env } from "@/env";
|
||||
|
||||
export default function WorkspacePage() {
|
||||
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true") {
|
||||
const firstThread = fs
|
||||
.readdirSync(path.resolve(process.cwd(), "public/demo/threads"), {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.find((thread) => thread.isDirectory() && !thread.name.startsWith("."));
|
||||
if (firstThread) {
|
||||
return redirect(`/workspace/chats/${firstThread.name}`);
|
||||
}
|
||||
}
|
||||
return redirect("/workspace/chats/new");
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -4,8 +4,6 @@ export function getBackendBaseURL() {
|
||||
if (env.NEXT_PUBLIC_BACKEND_BASE_URL) {
|
||||
return env.NEXT_PUBLIC_BACKEND_BASE_URL;
|
||||
} else {
|
||||
// Use empty string for same-origin requests through nginx
|
||||
// Paths like /api/models will be handled by nginx proxy
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export const enUS: Translations = {
|
||||
artifacts: "Artifacts",
|
||||
public: "Public",
|
||||
custom: "Custom",
|
||||
notAvailableInDemoMode: "Not available in demo mode",
|
||||
},
|
||||
|
||||
// Welcome
|
||||
@@ -57,6 +58,7 @@ export const enUS: Translations = {
|
||||
newChat: "New chat",
|
||||
chats: "Chats",
|
||||
recentChats: "Recent chats",
|
||||
demoChats: "Demo chats",
|
||||
},
|
||||
|
||||
// Breadcrumb
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface Translations {
|
||||
artifacts: string;
|
||||
public: string;
|
||||
custom: string;
|
||||
notAvailableInDemoMode: string;
|
||||
};
|
||||
|
||||
// Welcome
|
||||
@@ -52,6 +53,7 @@ export interface Translations {
|
||||
recentChats: string;
|
||||
newChat: string;
|
||||
chats: string;
|
||||
demoChats: string;
|
||||
};
|
||||
|
||||
// Breadcrumb
|
||||
|
||||
@@ -20,6 +20,7 @@ export const zhCN: Translations = {
|
||||
artifacts: "文件",
|
||||
public: "公共",
|
||||
custom: "自定义",
|
||||
notAvailableInDemoMode: "在演示模式下不可用",
|
||||
},
|
||||
|
||||
// Welcome
|
||||
@@ -54,7 +55,8 @@ export const zhCN: Translations = {
|
||||
sidebar: {
|
||||
newChat: "新对话",
|
||||
chats: "对话",
|
||||
recentChats: "最近的聊天",
|
||||
recentChats: "最近的对话",
|
||||
demoChats: "演示对话",
|
||||
},
|
||||
|
||||
// Breadcrumb
|
||||
|
||||
Reference in New Issue
Block a user