更新功能展示
@@ -187,6 +187,10 @@ npm run dev
|
||||
</script>
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- **知识库 / 内部 Wiki 导入(项目总览一篇通)**:[doc/AI-CS-知识库-项目总览.md](doc/AI-CS-知识库-项目总览.md)
|
||||
|
||||
## 常见问题与排障(先看这里)
|
||||
|
||||
- **提示音听不到**:浏览器通常需要“用户一次交互”才能解锁音频;请先点一下页面任意按钮/再打开喇叭开关测试
|
||||
@@ -203,4 +207,4 @@ npm run dev
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-03-25
|
||||
**最后更新**:2026-03-27
|
||||
|
||||
@@ -125,35 +125,45 @@ export default function KnowledgePage(props: any = {}) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 加载文档列表
|
||||
const loadDocuments = useCallback(async () => {
|
||||
if (!selectedKnowledgeBase) {
|
||||
setDocuments([]);
|
||||
setDocumentResult(null);
|
||||
return;
|
||||
}
|
||||
// 加载文档列表(silent:后台轮询向量化状态时不全屏“加载中”、不弹 Toast,避免刷屏)
|
||||
const loadDocuments = useCallback(
|
||||
async (opts?: { silent?: boolean }) => {
|
||||
if (!selectedKnowledgeBase) {
|
||||
setDocuments([]);
|
||||
setDocumentResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingDocs(true);
|
||||
try {
|
||||
const status = statusFilter === "all" ? undefined : statusFilter;
|
||||
const result = await fetchDocuments(
|
||||
selectedKnowledgeBase.id,
|
||||
currentPage,
|
||||
pageSize,
|
||||
searchKeyword || undefined,
|
||||
status
|
||||
);
|
||||
setDocumentResult(result);
|
||||
setDocuments(result.documents ?? []);
|
||||
} catch (error) {
|
||||
console.error("加载文档列表失败:", error);
|
||||
toast.error((error as Error).message || "加载文档列表失败");
|
||||
setDocuments([]);
|
||||
setDocumentResult(null);
|
||||
} finally {
|
||||
setLoadingDocs(false);
|
||||
}
|
||||
}, [selectedKnowledgeBase, currentPage, searchKeyword, statusFilter]);
|
||||
const silent = opts?.silent === true;
|
||||
if (!silent) {
|
||||
setLoadingDocs(true);
|
||||
}
|
||||
try {
|
||||
const status = statusFilter === "all" ? undefined : statusFilter;
|
||||
const result = await fetchDocuments(
|
||||
selectedKnowledgeBase.id,
|
||||
currentPage,
|
||||
pageSize,
|
||||
searchKeyword || undefined,
|
||||
status
|
||||
);
|
||||
setDocumentResult(result);
|
||||
setDocuments(result.documents ?? []);
|
||||
} catch (error) {
|
||||
console.error("加载文档列表失败:", error);
|
||||
if (!silent) {
|
||||
toast.error((error as Error).message || "加载文档列表失败");
|
||||
setDocuments([]);
|
||||
setDocumentResult(null);
|
||||
}
|
||||
} finally {
|
||||
if (!silent) {
|
||||
setLoadingDocs(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedKnowledgeBase, currentPage, searchKeyword, statusFilter]
|
||||
);
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
@@ -163,9 +173,25 @@ export default function KnowledgePage(props: any = {}) {
|
||||
// 当选择知识库或搜索条件变化时,重新加载文档
|
||||
useEffect(() => {
|
||||
setCurrentPage(1); // 切换知识库或搜索时重置页码
|
||||
loadDocuments();
|
||||
void loadDocuments();
|
||||
}, [loadDocuments]);
|
||||
|
||||
// 向量化进行中时自动刷新列表(pending → processing → completed/failed),无需手动整页刷新
|
||||
useEffect(() => {
|
||||
if (!selectedKnowledgeBase) return;
|
||||
const hasEmbeddingInFlight = documents.some(
|
||||
(d) => d.embedding_status === "pending" || d.embedding_status === "processing"
|
||||
);
|
||||
if (!hasEmbeddingInFlight) return;
|
||||
|
||||
const intervalMs = 2500;
|
||||
const id = window.setInterval(() => {
|
||||
void loadDocuments({ silent: true });
|
||||
}, intervalMs);
|
||||
|
||||
return () => window.clearInterval(id);
|
||||
}, [selectedKnowledgeBase, documents, loadDocuments]);
|
||||
|
||||
// 选择知识库
|
||||
const handleSelectKnowledgeBase = (kb: KnowledgeBase) => {
|
||||
setSelectedKnowledgeBase(kb);
|
||||
|
||||
@@ -48,7 +48,8 @@ export function ScreenshotDisplay({
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
|
||||
priority={false}
|
||||
unoptimized={false}
|
||||
// 营销截图常替换;走 /_next/image 会强缓存优化结果,本地改 public 后仍像旧图
|
||||
unoptimized
|
||||
/>
|
||||
{!imageLoaded && !imageError && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-primary/10 to-primary/5">
|
||||
|
||||
@@ -15,10 +15,12 @@ import {
|
||||
LayoutDashboard,
|
||||
FileText,
|
||||
ArrowRight,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
MessageSquare,
|
||||
Github,
|
||||
Mail,
|
||||
} from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ScreenshotDisplay } from "@/components/ScreenshotDisplay";
|
||||
import { ChatWidget } from "@/components/visitor/ChatWidget";
|
||||
import { FloatingButton } from "@/components/visitor/FloatingButton";
|
||||
@@ -82,9 +84,93 @@ const steps = [
|
||||
},
|
||||
];
|
||||
|
||||
const screenshotCards = [
|
||||
{
|
||||
key: "dashboard",
|
||||
title: "工作台",
|
||||
imageName: "dashboard.png",
|
||||
placeholderIcon: LayoutDashboard,
|
||||
placeholderText: "工作台界面",
|
||||
alt: "AI-CS 工作台界面",
|
||||
},
|
||||
{
|
||||
key: "visitor",
|
||||
title: "访客端",
|
||||
imageName: "visitor.png",
|
||||
placeholderIcon: Globe,
|
||||
placeholderText: "访客端界面",
|
||||
alt: "AI-CS 访客端界面",
|
||||
},
|
||||
{
|
||||
key: "ai-config",
|
||||
title: "AI配置",
|
||||
imageName: "ai-config.png",
|
||||
placeholderIcon: Bot,
|
||||
placeholderText: "AI配置界面",
|
||||
alt: "AI-CS AI配置界面",
|
||||
},
|
||||
{
|
||||
key: "users",
|
||||
title: "用户管理",
|
||||
imageName: "users.png",
|
||||
placeholderIcon: Users,
|
||||
placeholderText: "用户管理界面",
|
||||
alt: "AI-CS 用户管理界面",
|
||||
},
|
||||
{
|
||||
key: "faq",
|
||||
title: "FAQ管理",
|
||||
imageName: "faq.png",
|
||||
placeholderIcon: FileText,
|
||||
placeholderText: "FAQ管理界面",
|
||||
alt: "AI-CS FAQ管理界面",
|
||||
},
|
||||
{
|
||||
key: "knowledge",
|
||||
title: "知识库管理",
|
||||
imageName: "knowledge.png",
|
||||
placeholderIcon: BookOpen,
|
||||
placeholderText: "知识库管理界面",
|
||||
alt: "AI-CS 知识库管理界面",
|
||||
},
|
||||
{
|
||||
key: "conversations",
|
||||
title: "知识库测试",
|
||||
imageName: "conversations.png",
|
||||
placeholderIcon: MessageSquare,
|
||||
placeholderText: "知识库测试界面",
|
||||
alt: "AI-CS 知识库测试界面",
|
||||
},
|
||||
{
|
||||
key: "prompts",
|
||||
title: "提示词工程",
|
||||
imageName: "prompts.png",
|
||||
placeholderIcon: Wand2,
|
||||
placeholderText: "提示词工程界面",
|
||||
alt: "AI-CS 提示词工程界面",
|
||||
},
|
||||
{
|
||||
key: "logs",
|
||||
title: "日志中心",
|
||||
imageName: "logs.png",
|
||||
placeholderIcon: ScrollText,
|
||||
placeholderText: "日志中心界面",
|
||||
alt: "AI-CS 日志中心界面",
|
||||
},
|
||||
{
|
||||
key: "analytics",
|
||||
title: "可视化报表",
|
||||
imageName: "analytics.png",
|
||||
placeholderIcon: LineChart,
|
||||
placeholderText: "可视化报表界面",
|
||||
alt: "AI-CS 可视化报表界面",
|
||||
},
|
||||
];
|
||||
|
||||
export function HomePageClient() {
|
||||
const [visitorId, setVisitorId] = useState<number | null>(null);
|
||||
const [isChatOpen, setIsChatOpen] = useState(false);
|
||||
const [activeScreenshot, setActiveScreenshot] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let stored = window.localStorage.getItem("visitor_id");
|
||||
@@ -106,6 +192,26 @@ export function HomePageClient() {
|
||||
}
|
||||
};
|
||||
|
||||
const totalScreenshots = screenshotCards.length;
|
||||
const prevScreenshotIndex =
|
||||
(activeScreenshot - 1 + totalScreenshots) % totalScreenshots;
|
||||
const nextScreenshotIndex = (activeScreenshot + 1) % totalScreenshots;
|
||||
|
||||
const goPrevScreenshot = () => {
|
||||
setActiveScreenshot((prev) => (prev - 1 + totalScreenshots) % totalScreenshots);
|
||||
};
|
||||
|
||||
const goNextScreenshot = () => {
|
||||
setActiveScreenshot((prev) => (prev + 1) % totalScreenshots);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
setActiveScreenshot((prev) => (prev + 1) % totalScreenshots);
|
||||
}, 4800);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [totalScreenshots]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<Header />
|
||||
@@ -232,90 +338,78 @@ export function HomePageClient() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<Tabs defaultValue="dashboard" className="w-full">
|
||||
<TabsList className="mb-8 grid w-full grid-cols-3 md:grid-cols-5 rounded-xl bg-muted/50 p-1.5">
|
||||
<TabsTrigger
|
||||
value="dashboard"
|
||||
className="text-[15px] rounded-md px-3.5 py-2 transition-colors hover:bg-background/60"
|
||||
<div className="mb-8 flex flex-wrap justify-center gap-2">
|
||||
{screenshotCards.map((item, idx) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => setActiveScreenshot(idx)}
|
||||
className={`rounded-full px-4 py-1.5 text-sm transition-all ${
|
||||
idx === activeScreenshot
|
||||
? "bg-blue-600 text-white shadow-sm"
|
||||
: "bg-background text-muted-foreground border border-border/70 hover:text-foreground hover:border-blue-200"
|
||||
}`}
|
||||
>
|
||||
工作台
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="visitor"
|
||||
className="text-[15px] rounded-md px-3.5 py-2 transition-colors hover:bg-background/60"
|
||||
{item.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto max-w-6xl px-4 md:px-8">
|
||||
<div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(80%_60%_at_50%_40%,rgba(37,99,235,0.14),transparent_70%)]" />
|
||||
|
||||
<div className="relative h-[290px] md:h-[420px] lg:h-[510px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={goPrevScreenshot}
|
||||
className="absolute left-0 top-1/2 z-40 -translate-y-1/2 rounded-full border border-border/70 bg-background/90 p-2.5 shadow-sm backdrop-blur transition hover:border-blue-200 hover:bg-background"
|
||||
aria-label="查看上一张"
|
||||
>
|
||||
访客端
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="ai-config"
|
||||
className="text-[15px] rounded-md px-3.5 py-2 transition-colors hover:bg-background/60"
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={goNextScreenshot}
|
||||
className="absolute right-0 top-1/2 z-40 -translate-y-1/2 rounded-full border border-border/70 bg-background/90 p-2.5 shadow-sm backdrop-blur transition hover:border-blue-200 hover:bg-background"
|
||||
aria-label="查看下一张"
|
||||
>
|
||||
AI配置
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="users"
|
||||
className="text-[15px] rounded-md px-3.5 py-2 transition-colors hover:bg-background/60"
|
||||
>
|
||||
用户管理
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="faq"
|
||||
className="text-[15px] rounded-md px-3.5 py-2 transition-colors hover:bg-background/60"
|
||||
>
|
||||
FAQ管理
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="dashboard" className="mt-0">
|
||||
<div className="overflow-hidden rounded-xl border border-border/60 shadow-sm">
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
{/* 左侧半露卡片 */}
|
||||
<div className="absolute left-6 right-[42%] top-7 z-10 hidden overflow-hidden rounded-2xl border border-border/60 bg-background/85 shadow-md md:block">
|
||||
<div className="pointer-events-none absolute inset-0 z-10 bg-background/18" />
|
||||
<ScreenshotDisplay
|
||||
imageName="dashboard.png"
|
||||
placeholderIcon={LayoutDashboard}
|
||||
placeholderText="工作台界面"
|
||||
alt="AI-CS 工作台界面"
|
||||
imageName={screenshotCards[prevScreenshotIndex].imageName}
|
||||
placeholderIcon={screenshotCards[prevScreenshotIndex].placeholderIcon}
|
||||
placeholderText={screenshotCards[prevScreenshotIndex].placeholderText}
|
||||
alt={screenshotCards[prevScreenshotIndex].alt}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="visitor" className="mt-0">
|
||||
<div className="overflow-hidden rounded-xl border border-border/60 shadow-sm">
|
||||
|
||||
{/* 右侧半露卡片 */}
|
||||
<div className="absolute left-[42%] right-6 top-7 z-10 hidden overflow-hidden rounded-2xl border border-border/60 bg-background/85 shadow-md md:block">
|
||||
<div className="pointer-events-none absolute inset-0 z-10 bg-background/18" />
|
||||
<ScreenshotDisplay
|
||||
imageName="visitor.png"
|
||||
placeholderIcon={Globe}
|
||||
placeholderText="访客端界面"
|
||||
alt="AI-CS 访客端界面"
|
||||
imageName={screenshotCards[nextScreenshotIndex].imageName}
|
||||
placeholderIcon={screenshotCards[nextScreenshotIndex].placeholderIcon}
|
||||
placeholderText={screenshotCards[nextScreenshotIndex].placeholderText}
|
||||
alt={screenshotCards[nextScreenshotIndex].alt}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="ai-config" className="mt-0">
|
||||
<div className="overflow-hidden rounded-xl border border-border/60 shadow-sm">
|
||||
|
||||
{/* 中间主卡片 */}
|
||||
<div className="absolute inset-x-8 top-0 z-30 overflow-hidden rounded-2xl border border-border/70 bg-background shadow-xl ring-1 ring-blue-100/60 md:inset-x-16 lg:inset-x-24">
|
||||
<ScreenshotDisplay
|
||||
imageName="ai-config.png"
|
||||
placeholderIcon={Bot}
|
||||
placeholderText="AI配置界面"
|
||||
alt="AI-CS AI配置界面"
|
||||
imageName={screenshotCards[activeScreenshot].imageName}
|
||||
placeholderIcon={screenshotCards[activeScreenshot].placeholderIcon}
|
||||
placeholderText={screenshotCards[activeScreenshot].placeholderText}
|
||||
alt={screenshotCards[activeScreenshot].alt}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="users" className="mt-0">
|
||||
<div className="overflow-hidden rounded-xl border border-border/60 shadow-sm">
|
||||
<ScreenshotDisplay
|
||||
imageName="users.png"
|
||||
placeholderIcon={Users}
|
||||
placeholderText="用户管理界面"
|
||||
alt="AI-CS 用户管理界面"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="faq" className="mt-0">
|
||||
<div className="overflow-hidden rounded-xl border border-border/60 shadow-sm">
|
||||
<ScreenshotDisplay
|
||||
imageName="faq.png"
|
||||
placeholderIcon={FileText}
|
||||
placeholderText="FAQ管理界面"
|
||||
alt="AI-CS FAQ管理界面"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -24,9 +24,7 @@ export const websiteConfig = {
|
||||
// 友情链接(用于互相引流)
|
||||
// 格式:{ name: "链接名称", url: "链接地址" }
|
||||
friendLinks: [
|
||||
// 示例:添加您的友情链接
|
||||
// { name: "合作伙伴1", url: "https://example.com" },
|
||||
// { name: "合作伙伴2", url: "https://example2.com" },
|
||||
{ name: "Poixe免费API赞助", url: "https://poixe.com/products/free" },
|
||||
] as Array<{ name: string; url: string }>,
|
||||
|
||||
// 其他配置
|
||||
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 325 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 433 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 546 KiB |
|
After Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 510 KiB |