feat: add SupportHelpCenter component and logo image for enhanced support page

This commit is contained in:
mlogclub
2026-08-09 19:29:07 +08:00
parent acae165302
commit f4ff1c5839
3 changed files with 150 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
import { SupportHelpCenter } from "@/components/support-center/help-center"
export default function SupportPage() {
return <SupportHelpCenter />
}
@@ -0,0 +1,145 @@
"use client"
import {
ArrowRightIcon,
BookOpenIcon,
BotIcon,
ChevronDownIcon,
CircleHelpIcon,
FileTextIcon,
HeadphonesIcon,
LightbulbIcon,
MessageCircleMoreIcon,
SearchIcon,
SparklesIcon,
TicketCheckIcon,
WrenchIcon,
type LucideIcon,
} from "lucide-react"
import { useMemo, useState } from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { cn } from "@/lib/utils"
type Category = {
id: string
title: string
description: string
icon: LucideIcon
accent: string
articles: number
}
type Article = {
title: string
description: string
category: string
readTime: string
}
type Question = {
question: string
answer: string
category: string
}
const categories: Category[] = [
{ id: "getting-started", title: "快速开始", description: "部署、登录和首次配置", icon: SparklesIcon, accent: "bg-sky-500/10 text-sky-600 dark:text-sky-400", articles: 8 },
{ id: "ai", title: "AI 与模型配置", description: "模型、AI Agent 与工作流", icon: BotIcon, accent: "bg-violet-500/10 text-violet-600 dark:text-violet-400", articles: 12 },
{ id: "knowledge", title: "知识库与检索", description: "FAQ、文档与回答质量", icon: BookOpenIcon, accent: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400", articles: 10 },
{ id: "conversation", title: "会话与工单", description: "转人工、分配与问题闭环", icon: TicketCheckIcon, accent: "bg-amber-500/10 text-amber-600 dark:text-amber-400", articles: 9 },
{ id: "integration", title: "渠道接入", description: "Web Widget 与第三方渠道", icon: MessageCircleMoreIcon, accent: "bg-rose-500/10 text-rose-600 dark:text-rose-400", articles: 11 },
{ id: "troubleshooting", title: "故障排查", description: "定位常见配置与运行问题", icon: WrenchIcon, accent: "bg-slate-500/10 text-slate-600 dark:text-slate-400", articles: 14 },
]
const articles: Article[] = [
{ title: "用 Docker Compose 在 10 分钟内启动 AgentDesk", description: "了解启动依赖、管理后台入口和首次检查项。", category: "getting-started", readTime: "5 分钟" },
{ title: "如何配置第一个 AI Agent", description: "连接模型、知识库和转人工策略,完成一条可运行的服务流程。", category: "ai", readTime: "6 分钟" },
{ title: "FAQ 知识库和文档知识库有什么区别?", description: "按内容形态选择更适合的知识沉淀方式。", category: "knowledge", readTime: "4 分钟" },
{ title: "将 Web Widget 嵌入你的网站", description: "创建 Web 渠道、复制嵌入代码并完成上线前验证。", category: "integration", readTime: "7 分钟" },
{ title: "用户请求人工客服后会发生什么?", description: "理解接入池、客服状态、分配规则和上下文交接。", category: "conversation", readTime: "4 分钟" },
{ title: "Widget 能打开但不能发送消息怎么办?", description: "从渠道状态、域名配置和浏览器网络逐项排查。", category: "troubleshooting", readTime: "6 分钟" },
]
const questions: Question[] = [
{ question: "AgentDesk 适合哪些业务场景?", answer: "它适合希望把 AI 首次响应、知识库检索、人工接管和工单跟进连接起来的客服与服务团队。", category: "getting-started" },
{ question: "聊天模型可用,但知识库为什么没有命中?", answer: "通常需要检查 Embedding 模型、索引状态、检索阈值,以及 AI Agent 是否已绑定正确的知识库。", category: "knowledge" },
{ question: "如何把会话转给人工客服?", answer: "AI 或客服可以发起转人工;系统会依据服务时间、客服状态、团队和分配规则处理接入。", category: "conversation" },
{ question: "嵌入 Widget 时,channelId 是做什么的?", answer: "它标识一个具体 Web 渠道,并决定客户会话使用的渠道配置、品牌信息和身份校验策略。", category: "integration" },
]
export function SupportHelpCenter() {
const [query, setQuery] = useState("")
const [activeCategory, setActiveCategory] = useState("all")
const [openQuestion, setOpenQuestion] = useState(0)
const normalizedQuery = query.trim().toLocaleLowerCase()
const matches = useMemo(() => {
const matchesCategory = (category: string) => activeCategory === "all" || category === activeCategory
return {
articles: articles.filter((article) => matchesCategory(article.category) && `${article.title} ${article.description}`.toLocaleLowerCase().includes(normalizedQuery)),
questions: questions.filter((question) => matchesCategory(question.category) && `${question.question} ${question.answer}`.toLocaleLowerCase().includes(normalizedQuery)),
}
}, [activeCategory, normalizedQuery])
const isFiltering = activeCategory !== "all" || normalizedQuery.length > 0
return (
<main className="min-h-svh overflow-hidden bg-[#f7f9fc] text-foreground dark:bg-background">
<header className="mx-auto flex h-16 max-w-6xl items-center justify-between px-5 sm:px-8">
<a href="/support" className="flex items-center gap-2.5 font-semibold tracking-tight">
<span className="grid size-8 place-items-center rounded-xl bg-primary text-primary-foreground shadow-sm"><CircleHelpIcon className="size-[18px]" /></span>
<span>AgentDesk <span className="font-normal text-muted-foreground"></span></span>
</a>
<div className="flex items-center gap-1 sm:gap-2">
<Button variant="ghost" className="hidden sm:inline-flex" onClick={() => document.getElementById("popular-questions")?.scrollIntoView({ behavior: "smooth" })}></Button>
<Button variant="outline" onClick={() => document.getElementById("contact-support")?.scrollIntoView({ behavior: "smooth" })}><HeadphonesIcon /> </Button>
</div>
</header>
<section className="relative border-y border-sky-100 bg-[radial-gradient(circle_at_50%_-30%,#ddecff,transparent_55%)] px-5 py-15 sm:px-8 sm:py-20 dark:border-border dark:bg-[radial-gradient(circle_at_50%_-30%,rgba(36,117,252,.26),transparent_55%)]">
<div className="relative mx-auto max-w-3xl text-center">
<Badge variant="secondary" className="mb-5 bg-white/70 px-3 py-1 text-primary shadow-sm dark:bg-card/80">使</Badge>
<h1 className="text-balance text-3xl font-semibold tracking-tight sm:text-5xl"></h1>
<p className="mx-auto mt-4 max-w-xl text-pretty text-sm leading-6 text-muted-foreground sm:text-base"> AI AgentDesk</p>
<div className="relative mx-auto mt-8 max-w-2xl">
<SearchIcon className="pointer-events-none absolute top-1/2 left-4 size-5 -translate-y-1/2 text-muted-foreground" />
<Input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索问题,例如:如何接入 Web Widget" className="h-13 rounded-2xl border-white bg-white pl-12 pr-4 text-base shadow-[0_12px_30px_rgba(36,117,252,.12)] focus-visible:ring-primary/25 dark:border-border dark:bg-card" />
</div>
<div className="mt-4 flex flex-wrap justify-center gap-x-3 gap-y-1 text-xs text-muted-foreground"><span></span><button className="hover:text-primary" onClick={() => setQuery("部署")}></button><button className="hover:text-primary" onClick={() => setQuery("知识库")}></button><button className="hover:text-primary" onClick={() => setQuery("Widget")}>Web Widget</button></div>
</div>
</section>
<div className="mx-auto max-w-6xl px-5 py-10 sm:px-8 sm:py-14">
<section aria-labelledby="category-title">
<div className="mb-5 flex items-end justify-between gap-4"><div><p className="text-sm font-medium text-primary"></p><h2 id="category-title" className="mt-1 text-2xl font-semibold tracking-tight">使</h2></div><span className="hidden text-sm text-muted-foreground sm:block">6 · 64 </span></div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{categories.map((category) => { const Icon = category.icon; const active = activeCategory === category.id; return <button key={category.id} type="button" onClick={() => setActiveCategory(active ? "all" : category.id)} className={cn("group rounded-2xl border bg-card p-5 text-left shadow-sm transition-all hover:-translate-y-0.5 hover:shadow-md focus-visible:ring-3 focus-visible:ring-ring/50", active ? "border-primary ring-2 ring-primary/15" : "border-border")}><div className="flex items-start justify-between gap-4"><span className={cn("grid size-10 place-items-center rounded-xl", category.accent)}><Icon className="size-5" /></span><ArrowRightIcon className={cn("mt-1 size-4 text-muted-foreground transition-transform group-hover:translate-x-0.5", active && "text-primary")} /></div><h3 className="mt-5 font-medium">{category.title}</h3><p className="mt-1 text-sm text-muted-foreground">{category.description}</p><p className="mt-4 text-xs font-medium text-muted-foreground">{category.articles} </p></button> })}
</div>
</section>
<section className="mt-14" aria-labelledby="article-title">
<div className="mb-5 flex items-end justify-between gap-4"><div><p className="text-sm font-medium text-primary"></p><h2 id="article-title" className="mt-1 text-2xl font-semibold tracking-tight">{isFiltering ? "搜索结果" : "从这里开始"}</h2></div>{isFiltering && <Button variant="ghost" size="sm" onClick={() => { setQuery(""); setActiveCategory("all") }}></Button>}</div>
{matches.articles.length ? <div className="grid gap-3 lg:grid-cols-2">{matches.articles.map((article) => <Card key={article.title} className="gap-0 border border-border py-0 shadow-none transition-shadow hover:shadow-md"><CardContent className="flex items-center gap-4 p-5"><span className="grid size-10 shrink-0 place-items-center rounded-xl bg-muted text-muted-foreground"><FileTextIcon className="size-5" /></span><div className="min-w-0 flex-1"><h3 className="font-medium">{article.title}</h3><p className="mt-1 line-clamp-1 text-sm text-muted-foreground">{article.description}</p><p className="mt-2 text-xs text-muted-foreground"> {article.readTime}</p></div><Button variant="ghost" size="icon" aria-label={`查看 ${article.title}`}><ArrowRightIcon /></Button></CardContent></Card>)}</div> : <EmptyState />}
</section>
<section id="popular-questions" className="mt-14 grid gap-8 lg:grid-cols-[0.78fr_1.22fr] lg:items-start" aria-labelledby="faq-title">
<div className="rounded-3xl bg-slate-900 p-7 text-slate-50 dark:bg-primary"><LightbulbIcon className="size-6 text-sky-300" /><p className="mt-8 text-sm font-medium text-sky-200"></p><h2 id="faq-title" className="mt-2 text-2xl font-semibold tracking-tight"></h2><p className="mt-3 text-sm leading-6 text-slate-300">使</p></div>
<div className="divide-y divide-border rounded-2xl border bg-card px-5">{matches.questions.length ? matches.questions.map((item, index) => { const expanded = openQuestion === index; return <div key={item.question}><button type="button" className="flex w-full items-center justify-between gap-5 py-5 text-left font-medium" onClick={() => setOpenQuestion(expanded ? -1 : index)} aria-expanded={expanded}>{item.question}<ChevronDownIcon className={cn("size-4 shrink-0 text-muted-foreground transition-transform", expanded && "rotate-180")} /></button>{expanded && <p className="pb-5 pr-8 text-sm leading-6 text-muted-foreground">{item.answer}</p>}</div> }) : <EmptyState compact />}</div>
</section>
<section id="contact-support" className="mt-14 rounded-3xl border border-primary/15 bg-primary/[0.055] p-6 sm:flex sm:items-center sm:justify-between sm:p-8">
<div><p className="text-sm font-medium text-primary"></p><h2 className="mt-1 text-2xl font-semibold tracking-tight"></h2><p className="mt-2 max-w-xl text-sm leading-6 text-muted-foreground">线</p></div>
<Button size="lg" className="mt-5 sm:mt-0" onClick={() => window.alert("演示模式:后续将跳转到配置的在线支持渠道。")}>线 <ArrowRightIcon /></Button>
</section>
</div>
</main>
)
}
function EmptyState({ compact = false }: { compact?: boolean }) {
return <div className={cn("rounded-2xl border border-dashed bg-card p-8 text-center text-sm text-muted-foreground", compact && "border-0 p-5")}></div>
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB