Initial commit: AI Video Assistant fullstack platform.

Add pipecat-based backend with WebRTC/WS voice routes, Next.js frontend, and Docker Compose orchestration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Xin Wang
2026-06-08 13:51:28 +08:00
commit 42cab2a6ef
86 changed files with 19471 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,137 @@
import { ArrowRight, Boxes, GitBranch, Plus, Workflow } from "lucide-react";
import { Button } from "@/components/ui/button";
export function AssistantWorkflowPage() {
return (
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-8">
<div>
<h1 className="font-display display-lg text-ink"></h1>
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-muted-foreground">
AI
</p>
</div>
<section className="relative overflow-hidden rounded-3xl border border-hairline bg-canvas-soft p-10">
<div
aria-hidden
className="pointer-events-none absolute -right-20 -top-24 h-72 w-72 rounded-full opacity-55 blur-3xl"
style={{
backgroundImage:
"radial-gradient(circle, color-mix(in srgb, var(--gradient-mint) 55%, transparent), transparent 70%)",
}}
/>
<div className="relative flex items-start gap-5">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-surface-strong text-foreground">
<Workflow size={30} />
</div>
<div className="flex-1">
<div className="caption-label inline-flex rounded-full bg-surface-strong px-3 py-1 text-muted-foreground">
</div>
<h2 className="font-display display-sm mt-5 text-ink">
</h2>
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-body">
</p>
<div className="mt-7 flex gap-3">
<Button size="lg" className="gap-2">
<Plus size={16} />
</Button>
<Button
variant="outline"
size="lg"
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
>
<ArrowRight size={16} />
</Button>
</div>
</div>
</div>
</section>
<section className="grid grid-cols-3 gap-4">
<FeatureCard
icon={<GitBranch size={20} />}
title="节点编排"
description="通过拖拽节点组织多轮对话、判断分支和任务流转。"
/>
<FeatureCard
icon={<Boxes size={20} />}
title="组件复用"
description="复用模型、知识库、语音识别、声音资源和工具插件。"
/>
<FeatureCard
icon={<Workflow size={20} />}
title="流程调试"
description="支持逐节点测试、查看输入输出和定位失败原因。"
/>
</section>
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
<div className="mb-5">
<h2 className="font-display display-sm text-ink"></h2>
<p className="mt-1 text-sm text-muted-foreground">
React Flow
</p>
</div>
<div className="flex items-center gap-3 overflow-x-auto rounded-2xl border border-hairline bg-canvas-soft p-5">
{["开始", "意图识别", "知识库检索", "模型回答", "工具调用", "结束"].map(
(item, index) => (
<div key={item} className="flex items-center gap-3">
<div className="min-w-[128px] rounded-xl border border-hairline bg-card p-4 text-center shadow-sm">
<div className="text-sm font-medium text-foreground">
{item}
</div>
<div className="mt-1 text-xs text-muted-soft">
Node {index + 1}
</div>
</div>
{index < 5 && (
<ArrowRight size={18} className="shrink-0 text-muted-soft" />
)}
</div>
),
)}
</div>
</section>
</div>
);
}
function FeatureCard({
icon,
title,
description,
}: {
icon: React.ReactNode;
title: string;
description: string;
}) {
return (
<div className="rounded-2xl border border-hairline bg-card p-6 shadow-sm transition-shadow hover:shadow-md">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-full bg-surface-strong text-foreground">
{icon}
</div>
<div className="font-medium text-foreground">{title}</div>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
{description}
</p>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function ComponentsKnowledgePage() {
return (
<PlaceholderPage
title="知识库"
description="统一管理大语言模型、语音识别、声音资源、知识库与工具插件。"
/>
);
}

View File

@@ -0,0 +1,794 @@
"use client";
import {
Brain,
CheckCircle2,
ChevronLeft,
ChevronRight,
Eye,
EyeOff,
HelpCircle,
Loader2,
MoreHorizontal,
Pencil,
Plug,
Plus,
Search,
Trash2,
XCircle,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useState, type ReactNode } from "react";
type ModelType = "LLM" | "ASR" | "TTS" | "Realtime" | "Embedding";
type InterfaceType = "openai" | "xfyun" | "dashscope" | "gemini";
/** 各资源类型可选的接口类型 */
const interfaceOptionsByType: Record<ModelType, InterfaceType[]> = {
LLM: ["openai"],
ASR: ["openai", "xfyun", "dashscope"],
TTS: ["openai", "xfyun", "dashscope"],
Realtime: ["openai", "gemini"],
Embedding: ["openai"],
};
const modelTypes: ModelType[] = ["LLM", "ASR", "TTS", "Realtime", "Embedding"];
type ModelResource = {
id: string;
name: string;
modelId: string;
type: ModelType;
interfaceType: InterfaceType;
apiUrl: string;
apiKey: string;
};
const mockModels: ModelResource[] = [
{
id: "model_001",
name: "DeepSeek-V3",
modelId: "deepseek-chat",
type: "LLM",
interfaceType: "openai",
apiUrl: "https://api.deepseek.com/v1",
apiKey: "sk-deepseek-7f3a9c2e1b",
},
{
id: "model_002",
name: "Qwen-Max",
modelId: "qwen-max",
type: "LLM",
interfaceType: "openai",
apiUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
apiKey: "sk-qwen-4d8e2a6f0c",
},
{
id: "model_003",
name: "讯飞语音识别",
modelId: "iat",
type: "ASR",
interfaceType: "xfyun",
apiUrl: "https://iat-api.xfyun.cn/v2/iat",
apiKey: "xf-asr-9b1c3d5e7a",
},
{
id: "model_004",
name: "Paraformer 识别",
modelId: "paraformer-realtime-v2",
type: "ASR",
interfaceType: "dashscope",
apiUrl: "https://dashscope.aliyuncs.com/api/v1/services/audio/asr",
apiKey: "sk-paraformer-2e4f6a",
},
{
id: "model_005",
name: "讯飞语音合成",
modelId: "tts",
type: "TTS",
interfaceType: "xfyun",
apiUrl: "https://tts-api.xfyun.cn/v2/tts",
apiKey: "xf-tts-6c8a0b2d4f",
},
{
id: "model_006",
name: "CosyVoice 合成",
modelId: "cosyvoice-v1",
type: "TTS",
interfaceType: "dashscope",
apiUrl: "https://dashscope.aliyuncs.com/api/v1/services/audio/tts",
apiKey: "sk-cosyvoice-1a3c5e",
},
{
id: "model_007",
name: "OpenAI TTS",
modelId: "tts-1",
type: "TTS",
interfaceType: "openai",
apiUrl: "https://api.openai.com/v1/audio/speech",
apiKey: "sk-openai-tts-8f0a2c",
},
{
id: "model_008",
name: "GPT Realtime",
modelId: "gpt-4o-realtime-preview",
type: "Realtime",
interfaceType: "openai",
apiUrl: "https://api.openai.com/v1/realtime",
apiKey: "sk-realtime-3b5d7f9a1c",
},
{
id: "model_009",
name: "Gemini Live",
modelId: "gemini-2.0-flash-live",
type: "Realtime",
interfaceType: "gemini",
apiUrl: "https://generativelanguage.googleapis.com/v1beta",
apiKey: "gm-live-5e7a9c1b3d",
},
{
id: "model_010",
name: "text-embedding-3",
modelId: "text-embedding-3-small",
type: "Embedding",
interfaceType: "openai",
apiUrl: "https://api.openai.com/v1/embeddings",
apiKey: "sk-embed-0c2e4a6f8b",
},
{
id: "model_011",
name: "Kimi-K2",
modelId: "moonshot-v1-8k",
type: "LLM",
interfaceType: "openai",
apiUrl: "https://api.moonshot.cn/v1",
apiKey: "sk-kimi-7a9c1e3b5d",
},
{
id: "model_012",
name: "BGE Embedding",
modelId: "bge-m3",
type: "Embedding",
interfaceType: "openai",
apiUrl: "https://api.siliconflow.cn/v1/embeddings",
apiKey: "sk-bge-2d4f6a8c0e",
},
];
type ModelForm = {
name: string;
modelId: string;
type: ModelType;
interfaceType: InterfaceType;
apiUrl: string;
apiKey: string;
};
const emptyForm: ModelForm = {
name: "",
modelId: "",
type: "LLM",
interfaceType: "openai",
apiUrl: "",
apiKey: "",
};
type TypeFilter = "全部" | ModelType;
const typeFilters: TypeFilter[] = ["全部", ...modelTypes];
export function ComponentsModelsPage() {
const [searchQuery, setSearchQuery] = useState("");
const [typeFilter, setTypeFilter] = useState<TypeFilter>("全部");
const [currentPage, setCurrentPage] = useState(1);
const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [form, setForm] = useState<ModelForm>(emptyForm);
const [showKey, setShowKey] = useState(false);
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<"idle" | "ok" | "fail">("idle");
function updateForm<K extends keyof ModelForm>(key: K, value: ModelForm[K]) {
setForm((prev) => ({ ...prev, [key]: value }));
// 任何配置变更后,旧的测试结果不再可信,重置为待测状态
setTestResult("idle");
}
async function handleTestConnection() {
setTesting(true);
setTestResult("idle");
try {
// TODO: 接入真实疎通接口(按 form.interfaceType 区分调用方式)
await new Promise((resolve) => setTimeout(resolve, 900));
const reachable = Boolean(form.apiUrl.trim() && form.apiKey.trim());
setTestResult(reachable ? "ok" : "fail");
} catch {
setTestResult("fail");
} finally {
setTesting(false);
}
}
function handleTypeChange(type: ModelType) {
const options = interfaceOptionsByType[type];
setForm((prev) => ({
...prev,
type,
// 资源类型变化时,若当前接口类型不在新类型的可选项内则重置为首项
interfaceType: options.includes(prev.interfaceType)
? prev.interfaceType
: options[0],
}));
setTestResult("idle");
}
function openCreate() {
setEditingId(null);
setForm(emptyForm);
setShowKey(false);
setTestResult("idle");
setDialogOpen(true);
}
function openEdit(model: ModelResource) {
setEditingId(model.id);
setForm({
name: model.name,
modelId: model.modelId,
type: model.type,
interfaceType: model.interfaceType,
apiUrl: model.apiUrl,
apiKey: model.apiKey,
});
setShowKey(false);
setTestResult("idle");
setDialogOpen(true);
}
const filteredModels = mockModels.filter((model) => {
if (typeFilter !== "全部" && model.type !== typeFilter) {
return false;
}
const keyword = searchQuery.trim().toLowerCase();
if (!keyword) {
return true;
}
return [model.name, model.modelId, model.type, model.interfaceType, model.apiUrl]
.join(" ")
.toLowerCase()
.includes(keyword);
});
const pageSize = 5;
const totalPages = Math.max(1, Math.ceil(filteredModels.length / pageSize));
const safeCurrentPage = Math.min(currentPage, totalPages);
const pageStart = (safeCurrentPage - 1) * pageSize;
const pageEnd = pageStart + pageSize;
const paginatedModels = filteredModels.slice(pageStart, pageEnd);
function handleSearchChange(value: string) {
setSearchQuery(value);
setCurrentPage(1);
}
function handleFilterChange(filter: TypeFilter) {
setTypeFilter(filter);
setCurrentPage(1);
}
const interfaceOptions = interfaceOptionsByType[form.type];
const canSave =
form.name.trim() && form.modelId.trim() && form.apiUrl.trim();
return (
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-8">
<div className="flex flex-col items-start justify-between gap-5 sm:flex-row sm:gap-6">
<div>
<h1 className="font-display display-lg text-ink"></h1>
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-muted-foreground">
LLMASRTTSRealtime Embedding 访
</p>
</div>
<Button
size="lg"
className="w-full shrink-0 gap-2 sm:w-auto"
onClick={openCreate}
>
<Plus size={16} />
</Button>
</div>
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
<div className="mb-6 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-wrap items-center gap-2">
{typeFilters.map((filter) => (
<Button
key={filter}
variant={filter === typeFilter ? "default" : "outline"}
size="sm"
className={
filter === typeFilter
? "rounded-full"
: "rounded-full border-hairline-strong text-muted-foreground hover:text-foreground"
}
onClick={() => handleFilterChange(filter)}
>
{filter}
</Button>
))}
</div>
<div className="relative w-full lg:w-[320px]">
<Search
size={15}
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-soft"
/>
<Input
value={searchQuery}
onChange={(event) => handleSearchChange(event.target.value)}
className="h-10 border-hairline-strong bg-background pl-9 text-sm text-foreground placeholder:text-muted-soft"
placeholder="搜索模型名称、接口类型或 URL..."
/>
</div>
</div>
<div className="overflow-hidden rounded-xl border border-hairline">
<div className="hidden items-center gap-4 bg-surface-strong/60 px-5 py-3 md:flex">
<div className="caption-label flex-1 text-muted-soft"></div>
<div className="caption-label w-[110px] text-muted-soft">
</div>
<div className="caption-label w-[110px] text-muted-soft">
</div>
<div className="caption-label w-[260px] text-muted-soft">
API URL
</div>
<div className="caption-label w-[116px] text-right text-muted-soft">
</div>
</div>
<div className="divide-y divide-hairline">
{paginatedModels.map((model) => (
<div
key={model.id}
className="flex flex-col gap-3 px-5 py-4 text-sm transition-colors hover:bg-surface-strong/40 md:flex-row md:items-center md:gap-4"
>
<div className="min-w-0 flex-1">
<div className="truncate font-medium text-foreground">
{model.name}
</div>
<div className="mt-1 text-xs text-muted-soft">
{model.modelId}
</div>
</div>
<div className="md:w-[110px]">
<Badge
variant="secondary"
className="h-6 bg-surface-strong px-3 text-muted-foreground"
>
{model.type}
</Badge>
</div>
<div className="md:w-[110px]">
<Badge
variant="secondary"
className="h-6 bg-surface-strong px-3 text-muted-foreground"
>
{model.interfaceType}
</Badge>
</div>
<div className="truncate text-muted-foreground md:w-[260px]">
{model.apiUrl}
</div>
<div className="flex justify-end gap-2 md:w-[116px]">
<Button
variant="outline"
size="sm"
className="gap-1.5 border-hairline-strong text-xs text-muted-foreground hover:text-foreground"
onClick={() => openEdit(model)}
>
<Pencil size={14} />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon-sm"
className="border-hairline-strong text-muted-foreground hover:text-foreground"
aria-label={`${model.name} 更多操作`}
>
<MoreHorizontal size={15} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-32 min-w-32 rounded-xl border border-hairline bg-popover p-1"
>
<DropdownMenuItem variant="destructive" className="rounded-lg">
<Trash2 size={14} />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
{filteredModels.length === 0 && (
<div className="px-5 py-12 text-center">
<div className="font-medium text-foreground">
</div>
<div className="mt-2 text-sm text-muted-foreground">
</div>
</div>
)}
</div>
</div>
<div className="mt-5 flex flex-col gap-3 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
<div>
{filteredModels.length === 0
? "没有数据"
: `显示 ${pageStart + 1}-${Math.min(pageEnd, filteredModels.length)} / 共 ${filteredModels.length} 个模型`}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon-sm"
className="border-hairline-strong text-muted-foreground hover:text-foreground"
disabled={safeCurrentPage <= 1}
onClick={() => setCurrentPage((page) => Math.max(1, page - 1))}
aria-label="上一页"
>
<ChevronLeft size={15} />
</Button>
{Array.from({ length: totalPages }, (_, index) => index + 1).map(
(page) => (
<Button
key={page}
variant={page === safeCurrentPage ? "default" : "outline"}
size="sm"
className={[
"h-8 min-w-8 px-2",
page === safeCurrentPage
? ""
: "border-hairline-strong text-muted-foreground hover:text-foreground",
].join(" ")}
onClick={() => setCurrentPage(page)}
>
{page}
</Button>
),
)}
<Button
variant="outline"
size="icon-sm"
className="border-hairline-strong text-muted-foreground hover:text-foreground"
disabled={safeCurrentPage >= totalPages}
onClick={() =>
setCurrentPage((page) => Math.min(totalPages, page + 1))
}
aria-label="下一页"
>
<ChevronRight size={15} />
</Button>
</div>
</div>
</section>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-h-[calc(100vh-3rem)] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{editingId ? "编辑模型" : "添加模型"}</DialogTitle>
<DialogDescription>
ID访
</DialogDescription>
</DialogHeader>
<div className="space-y-5">
<div className="block">
<FieldLabel
htmlFor="model-name"
hint={{
description:
"在控制台中展示的资源别名,便于识别和管理,可与模型 ID 不同。",
example: "DeepSeek-V3、Qwen-Max",
}}
>
</FieldLabel>
<Input
id="model-name"
value={form.name}
autoFocus
onChange={(event) => updateForm("name", event.target.value)}
placeholder="请输入资源名称"
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
</div>
<div className="block">
<FieldLabel
htmlFor="model-id"
hint={{
description:
"调用 API 时传入的模型标识,需与服务商文档中的名称完全一致。",
example: "deepseek-chat、qwen-max、paraformer-realtime-v2",
}}
>
ID
</FieldLabel>
<Input
id="model-id"
value={form.modelId}
onChange={(event) =>
updateForm("modelId", event.target.value)
}
placeholder="例如 deepseek-chat"
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
</div>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div className="block">
<FieldLabel
hint={{
description:
"模型在语音管线中的能力分类,决定可选的接口类型与后续编排用途。",
example: "LLM、ASR、TTS、Realtime、Embedding",
}}
>
</FieldLabel>
<Select
value={form.type}
onValueChange={(value) => handleTypeChange(value as ModelType)}
>
<SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
<SelectValue placeholder="请选择资源类型" />
</SelectTrigger>
<SelectContent className="border-hairline bg-popover text-popover-foreground">
{modelTypes.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="block">
<FieldLabel
hint={{
description:
"服务商 API 的协议或适配器类型,需与所选资源类型匹配。",
example: "openai、xfyun、dashscope、gemini",
}}
>
</FieldLabel>
<Select
value={form.interfaceType}
onValueChange={(value) =>
updateForm("interfaceType", value as InterfaceType)
}
>
<SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
<SelectValue placeholder="请选择接口类型" />
</SelectTrigger>
<SelectContent className="border-hairline bg-popover text-popover-foreground">
{interfaceOptions.map((item) => (
<SelectItem key={item} value={item}>
{item}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="block">
<FieldLabel
htmlFor="model-api-url"
hint={{
description:
"模型服务的 Base URL 或接口根地址,通常以 /v1 结尾,不含具体路径参数。",
example: "https://api.deepseek.com/v1",
}}
>
API URL
</FieldLabel>
<Input
id="model-api-url"
value={form.apiUrl}
onChange={(event) => updateForm("apiUrl", event.target.value)}
placeholder="https://api.example.com/v1"
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
</div>
<div className="block">
<FieldLabel
htmlFor="model-api-key"
hint={{
description:
"访问模型服务的鉴权密钥,由服务商控制台生成,请妥善保管勿泄露。",
example: "sk-xxxxxxxx",
}}
>
API Key
</FieldLabel>
<div className="relative">
<Input
id="model-api-key"
value={form.apiKey}
type={showKey ? "text" : "password"}
onChange={(event) => updateForm("apiKey", event.target.value)}
placeholder="请输入 API Key"
className="border-hairline-strong bg-background pr-10 text-foreground placeholder:text-muted-soft"
/>
<button
type="button"
onClick={() => setShowKey((value) => !value)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-soft transition-colors hover:text-foreground"
aria-label={showKey ? "隐藏 API Key" : "显示 API Key"}
>
{showKey ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
</div>
<DialogFooter className="sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<Button
variant="outline"
size="sm"
className="gap-2 border-hairline-strong text-muted-foreground hover:text-foreground"
disabled={!canSave || testing}
onClick={handleTestConnection}
>
{testing ? (
<Loader2 size={14} className="animate-spin" />
) : (
<Plug size={14} />
)}
</Button>
{testResult === "ok" && (
<span className="flex items-center gap-1.5 text-xs text-emerald-500">
<CheckCircle2 size={14} />
</span>
)}
{testResult === "fail" && (
<span className="flex items-center gap-1.5 text-xs text-destructive">
<XCircle size={14} />
</span>
)}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
className="border-hairline-strong text-muted-foreground hover:text-foreground"
onClick={() => setDialogOpen(false)}
>
</Button>
<Button
className="gap-2"
disabled={!canSave}
onClick={() => setDialogOpen(false)}
>
<Brain size={16} />
{editingId ? "保存" : "添加"}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
function FieldLabel({
htmlFor,
children,
hint,
}: {
htmlFor?: string;
children: ReactNode;
hint: { description: string; example?: string };
}) {
return (
<div className="mb-2 flex items-center gap-1.5 text-sm font-medium text-foreground">
<label htmlFor={htmlFor}>{children}</label>
<HelpHint {...hint} />
</div>
);
}
function HelpHint({
description,
example,
}: {
description: string;
example?: string;
}) {
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
aria-label="查看说明"
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
>
<HelpCircle size={14} />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-72 space-y-2 text-sm leading-6 text-muted-foreground"
>
<p>{description}</p>
{example && (
<p className="text-xs leading-5 text-muted-soft">
<span className="font-medium text-muted-foreground"></span>
{example}
</p>
)}
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,10 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function ComponentsToolsPage() {
return (
<PlaceholderPage
title="工具资源"
description="统一管理大语言模型、语音识别、声音资源、知识库与工具插件。"
/>
);
}

View File

@@ -0,0 +1,10 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function DashboardPage() {
return (
<PlaceholderPage
title="数据看板"
description="查看助手的对话数据、运行日志与调用明细。"
/>
);
}

View File

@@ -0,0 +1,10 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function HistoryPage() {
return (
<PlaceholderPage
title="历史记录"
description="查看助手的对话历史、运行日志与调用明细。"
/>
);
}

View File

@@ -0,0 +1,65 @@
import { Boxes, Plus } from "lucide-react";
import type { NavKey } from "@/components/layout/AppShell";
import { Button } from "@/components/ui/button";
type HomePageProps = {
onNavigate: (key: NavKey) => void;
};
export function HomePage({ onNavigate }: HomePageProps) {
return (
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-12 pt-[3vh]">
{/* Hero band — atmospheric gradient orbs behind editorial display copy */}
<section className="relative overflow-hidden rounded-3xl border border-hairline bg-canvas-soft px-10 py-16">
<div
aria-hidden
className="pointer-events-none absolute -right-24 -top-24 h-80 w-80 rounded-full opacity-60 blur-3xl"
style={{
backgroundImage:
"radial-gradient(circle, color-mix(in srgb, var(--gradient-sky) 55%, transparent), transparent 70%)",
}}
/>
<div
aria-hidden
className="pointer-events-none absolute -bottom-28 left-10 h-72 w-72 rounded-full opacity-50 blur-3xl"
style={{
backgroundImage:
"radial-gradient(circle, color-mix(in srgb, var(--gradient-lavender) 55%, transparent), transparent 70%)",
}}
/>
<div className="relative">
<h1 className="font-display display-xl text-ink">
</h1>
<p className="mt-6 max-w-xl text-[16px] leading-7 text-body">
AI
</p>
<div className="mt-9 flex gap-3">
<Button
size="lg"
className="gap-2 px-5"
onClick={() => onNavigate("assistants")}
>
<Plus size={17} />
</Button>
<Button
variant="outline"
size="lg"
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
onClick={() => onNavigate("components-models")}
>
<Boxes size={17} />
</Button>
</div>
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,47 @@
type PlaceholderPageProps = {
title: string;
description: string;
};
export function PlaceholderPage({ title, description }: PlaceholderPageProps) {
return (
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-8">
<div>
<h1 className="font-display display-lg text-ink">{title}</h1>
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-muted-foreground">
{description}
</p>
</div>
<section className="relative overflow-hidden rounded-3xl border border-hairline bg-canvas-soft px-10 py-20 text-center">
<div
aria-hidden
className="pointer-events-none absolute -right-24 top-0 h-72 w-72 rounded-full opacity-50 blur-3xl"
style={{
backgroundImage:
"radial-gradient(circle, color-mix(in srgb, var(--gradient-sky) 50%, transparent), transparent 70%)",
}}
/>
<div
aria-hidden
className="pointer-events-none absolute -left-20 bottom-0 h-64 w-64 rounded-full opacity-45 blur-3xl"
style={{
backgroundImage:
"radial-gradient(circle, color-mix(in srgb, var(--gradient-lavender) 50%, transparent), transparent 70%)",
}}
/>
<div className="relative">
<div className="caption-label inline-flex rounded-full bg-surface-strong px-3 py-1 text-muted-foreground">
</div>
<p className="font-display display-sm mx-auto mt-5 max-w-md text-ink">
</p>
<p className="mx-auto mt-3 max-w-md text-sm leading-7 text-body">
</p>
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function ProfilePage() {
return (
<PlaceholderPage
title="个人中心"
description="管理账户信息、团队成员、密钥与偏好设置。"
/>
);
}

View File

@@ -0,0 +1,10 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function TestPage() {
return (
<PlaceholderPage
title="测试助手"
description="在发布前通过实时视频对话测试助手的表现与交互体验。"
/>
);
}

View File

@@ -0,0 +1,10 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function WorkflowPage() {
return (
<PlaceholderPage
title="工作流"
description="管理与编排可复用的助手工作流,支持多轮任务与工具调用。"
/>
);
}