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

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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