mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-06 12:59:09 +00:00
fix(frontend): add public case study routes (#4635)
This commit is contained in:
parent
d732b90dc3
commit
480a3757ed
@ -19,6 +19,7 @@ https://github.com/user-attachments/assets/a8bcadc4-e040-4cf2-8fda-dd768b999c18
|
||||
## Official Website
|
||||
|
||||
Learn more and see **real demos** on our [**official website**](https://deerflow.tech).
|
||||
The landing-page case studies open as allowlisted, read-only showcases without requiring a sign-in.
|
||||
|
||||
## Sister Projects
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ The frontend is a stateful chat application. Users create **threads** (conversat
|
||||
|
||||
### Source Layout (`src/`)
|
||||
|
||||
- **`app/`** — Next.js App Router. Routes include `/` (landing), `/workspace/chats/[thread_id]` (chat), `/workspace/agents/[agent_name]` and `/workspace/agents/new` (custom agents), `/blog/…`, the `(auth)/{login,setup,auth/callback}` flow, `/[lang]/docs/…`, and `/api/…` route handlers (e.g. `/api/memory`).
|
||||
- **`app/`** — Next.js App Router. Routes include `/` (landing), `/showcase/[thread_id]` (allowlisted public read-only demos), `/workspace/chats/[thread_id]` (authenticated chat), `/workspace/agents/[agent_name]` and `/workspace/agents/new` (custom agents), `/blog/…`, the `(auth)/{login,setup,auth/callback}` flow, `/[lang]/docs/…`, and `/api/…` route handlers (e.g. `/api/memory`).
|
||||
- **`components/`** — React components:
|
||||
- `ui/` — Shadcn UI primitives (auto-generated, ESLint-ignored)
|
||||
- `ai-elements/` — Vercel AI SDK elements (auto-generated, ESLint-ignored)
|
||||
|
||||
@ -97,6 +97,7 @@ tests/
|
||||
src/
|
||||
├── app/ # Next.js App Router pages
|
||||
│ ├── api/ # API routes
|
||||
│ ├── showcase/ # Allowlisted public read-only demos
|
||||
│ ├── workspace/ # Main workspace pages
|
||||
│ └── mock/ # Mock/demo pages
|
||||
├── components/ # React components
|
||||
|
||||
34
frontend/src/app/showcase/[thread_id]/layout.tsx
Normal file
34
frontend/src/app/showcase/[thread_id]/layout.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import "katex/dist/katex.min.css";
|
||||
import "streamdown/styles.css";
|
||||
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
import { QueryClientProvider } from "@/components/query-client-provider";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { ChatProviders } from "@/components/workspace/chats/chat-providers";
|
||||
import { AuthProvider } from "@/core/auth/AuthProvider";
|
||||
import { I18nProvider } from "@/core/i18n/context";
|
||||
import { detectLocaleServer } from "@/core/i18n/server";
|
||||
|
||||
export default async function PublicShowcaseLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const locale = await detectLocaleServer();
|
||||
|
||||
return (
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<AuthProvider initialUser={null}>
|
||||
<QueryClientProvider>
|
||||
<SidebarProvider className="h-screen" defaultOpen={false}>
|
||||
<SidebarInset className="min-w-0">
|
||||
<ChatProviders>{children}</ChatProviders>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
<Toaster position="top-center" />
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
22
frontend/src/app/showcase/[thread_id]/page.tsx
Normal file
22
frontend/src/app/showcase/[thread_id]/page.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import ChatPage from "@/app/workspace/chats/[thread_id]/page";
|
||||
import { DEMO_THREAD_IDS, isDemoThreadId } from "@/core/threads/static-demo";
|
||||
|
||||
export const dynamicParams = false;
|
||||
|
||||
export function generateStaticParams() {
|
||||
return DEMO_THREAD_IDS.map((thread_id) => ({ thread_id }));
|
||||
}
|
||||
|
||||
export default async function PublicShowcasePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ thread_id: string }>;
|
||||
}) {
|
||||
const { thread_id: threadId } = await params;
|
||||
if (!isDemoThreadId(threadId)) {
|
||||
notFound();
|
||||
}
|
||||
return <ChatPage />;
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
import { ChatProviders } from "@/components/workspace/chats/chat-providers";
|
||||
import { isStaticWebsiteOnly } from "@/core/static-mode";
|
||||
import { DEMO_THREAD_IDS } from "@/core/threads/static-demo";
|
||||
|
||||
import { ChatProviders } from "./providers";
|
||||
|
||||
export function generateStaticParams() {
|
||||
if (!isStaticWebsiteOnly()) {
|
||||
return [];
|
||||
|
||||
@ -254,7 +254,7 @@ export default function ChatPage() {
|
||||
? localSettings.tokenUsage.inlineMode
|
||||
: "off";
|
||||
const hasTodos = (thread.values.todos?.length ?? 0) > 0;
|
||||
const browserEnabled = !isNewThread && browserControlEnabled;
|
||||
const browserEnabled = !isNewThread && !isMock && browserControlEnabled;
|
||||
const { activeGoal, hasGoal, setLocalGoal } = useActiveGoal(
|
||||
threadId,
|
||||
thread.values.goal,
|
||||
@ -285,12 +285,12 @@ export default function ChatPage() {
|
||||
: "bg-background/80 shadow-xs backdrop-blur",
|
||||
)}
|
||||
>
|
||||
<SidebarTrigger className="md:hidden" />
|
||||
{!isMock && <SidebarTrigger className="md:hidden" />}
|
||||
<div className="flex min-w-0 flex-1 items-center text-sm font-medium">
|
||||
<ThreadTitle threadId={threadId} thread={thread} />
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{!isNewThread && (
|
||||
{!isNewThread && !isMock && (
|
||||
<ThreadScheduledTasksLink threadId={threadId} />
|
||||
)}
|
||||
{tokenUsageEnabled ? (
|
||||
|
||||
@ -2,7 +2,7 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { pathOfThread } from "@/core/threads/utils";
|
||||
import { pathOfPublicDemoThread } from "@/core/threads/static-demo";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Section } from "../section";
|
||||
@ -56,7 +56,7 @@ export function CaseStudySection({ className }: { className?: string }) {
|
||||
{caseStudies.map((caseStudy) => (
|
||||
<Link
|
||||
key={caseStudy.title}
|
||||
href={pathOfThread(caseStudy.threadId) + "?mock=true"}
|
||||
href={pathOfPublicDemoThread(caseStudy.threadId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@ -119,7 +119,9 @@ export function useThreadChat() {
|
||||
setIsNewThreadState(nextIsNewThread);
|
||||
}, []);
|
||||
|
||||
const isMock = searchParams.get("mock") === "true";
|
||||
const isMock =
|
||||
actualPathname.startsWith("/showcase/") ||
|
||||
searchParams.get("mock") === "true";
|
||||
return {
|
||||
threadId: isNewPath ? (newThreadIdRef.current ?? threadId) : threadId,
|
||||
setThreadId,
|
||||
|
||||
@ -125,6 +125,16 @@ export function resolveStaticDemoArtifact(
|
||||
return `/demo/threads/${threadId}/${artifactPath}`;
|
||||
}
|
||||
|
||||
const DEMO_THREAD_ID_SET = new Set<string>(DEMO_THREAD_IDS);
|
||||
|
||||
export function isDemoThreadId(threadId: string): boolean {
|
||||
return DEMO_THREAD_ID_SET.has(threadId);
|
||||
}
|
||||
|
||||
export function pathOfPublicDemoThread(threadId: string): string {
|
||||
return `/showcase/${encodeURIComponent(threadId)}`;
|
||||
}
|
||||
|
||||
export type ThreadSearchParams = NonNullable<
|
||||
Parameters<ThreadsClient["search"]>[0]
|
||||
>;
|
||||
|
||||
@ -728,7 +728,7 @@ test.describe("Thread history", () => {
|
||||
},
|
||||
);
|
||||
|
||||
await page.goto(`/workspace/chats/${DEMO_THREAD_ID}?mock=true`);
|
||||
await page.goto(`/showcase/${DEMO_THREAD_ID}`);
|
||||
|
||||
await expect(
|
||||
page.getByText("What might be the trends and opportunities in 2026?"),
|
||||
@ -739,6 +739,12 @@ test.describe("Thread history", () => {
|
||||
expect(backendRunHistoryUrls).toEqual([]);
|
||||
});
|
||||
|
||||
test("public showcase rejects unknown thread IDs", async ({ page }) => {
|
||||
const response = await page.goto("/showcase/not-a-bundled-demo");
|
||||
|
||||
expect(response?.status()).toBe(404);
|
||||
});
|
||||
|
||||
test("chats list page shows all threads", async ({ page }) => {
|
||||
mockLangGraphAPI(page, { threads: THREADS });
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import { describe, expect, it } from "@rstest/core";
|
||||
|
||||
import {
|
||||
DEMO_THREAD_IDS,
|
||||
isDemoThreadId,
|
||||
pathOfPublicDemoThread,
|
||||
resolveStaticDemoArtifact,
|
||||
STATIC_DEMO_ARTIFACTS,
|
||||
} from "@/core/threads/static-demo";
|
||||
@ -65,3 +67,16 @@ describe("resolveStaticDemoArtifact", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("public demo threads", () => {
|
||||
it("recognizes only bundled demo thread IDs", () => {
|
||||
expect(isDemoThreadId(DEMO_THREAD_IDS[0])).toBe(true);
|
||||
expect(isDemoThreadId("not-a-demo-thread")).toBe(false);
|
||||
});
|
||||
|
||||
it("builds an encoded public route in mock mode", () => {
|
||||
expect(pathOfPublicDemoThread("thread/id?")).toBe(
|
||||
"/showcase/thread%2Fid%3F",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user