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