diff --git a/apps/web/app/wenfan-xiaobao/page.tsx b/apps/web/app/wenfan-xiaobao/page.tsx
new file mode 100644
index 0000000..70ff52a
--- /dev/null
+++ b/apps/web/app/wenfan-xiaobao/page.tsx
@@ -0,0 +1,187 @@
+'use client';
+
+import {
+ BotMessageSquare,
+ MessageCircleQuestionMark,
+ Mic,
+ Plus,
+ Search,
+ SendHorizontal,
+ SquarePen,
+} from 'lucide-react';
+
+const HISTORY_ITEMS = [
+ { title: '如何从产品创建到版本发布?', time: '今天' },
+ { title: '需求池的状态分别代表什么?', time: '今天' },
+ { title: '成员离职后怎么转交任务?', time: '昨天' },
+ { title: 'AI 配置和模型在哪里维护?', time: '昨天' },
+ { title: '测试用例失败后怎么提 Bug?', time: '周一' },
+];
+
+const QUICK_QUESTIONS = [
+ '怎么创建一个版本?',
+ '怎么把需求关联到项目?',
+ '小宝预警的红点是什么意思?',
+];
+
+const MESSAGES = [
+ {
+ role: 'assistant',
+ title: '问翻小宝',
+ content: '我是 FTB 的系统帮助助手。你可以直接问产品、项目、版本、需求、任务、测试、Bug、权限和 AI 配置相关的使用问题。',
+ },
+ {
+ role: 'user',
+ title: '我',
+ content: '怎么把需求放进某个版本里?',
+ },
+ {
+ role: 'assistant',
+ title: '问翻小宝',
+ content: '进入版本详情后,在需求区域选择当前项目下已采纳的需求;保存后,版本会基于这些需求继续承载开发任务、测试用例和发布风险分析。',
+ },
+];
+
+export default function WenfanXiaobaoPage() {
+ return (
+
+
+
+
+
+
+
+
+
+ {MESSAGES.map((message) => (
+
+ ))}
+
+
+ {QUICK_QUESTIONS.map((question) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
+
+function MessageRow({ message }: { message: { role: string; title: string; content: string } }) {
+ const isUser = message.role === 'user';
+
+ if (isUser) {
+ return (
+
+
+ {message.content}
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
{message.title}
+
{message.content}
+
+
+ );
+}
diff --git a/apps/web/components/layout/Sidebar.tsx b/apps/web/components/layout/Sidebar.tsx
index 6fdb07d..7545a3d 100644
--- a/apps/web/components/layout/Sidebar.tsx
+++ b/apps/web/components/layout/Sidebar.tsx
@@ -2,7 +2,7 @@
import { useEffect } from 'react';
import { usePathname, useRouter } from 'next/navigation';
-import { Inbox, Package, FolderKanban, Tag, Users, LayoutGrid, Lightbulb, Clock, Shield, Settings, Sparkles, TriangleAlert } from 'lucide-react';
+import { Inbox, Package, FolderKanban, Tag, Users, LayoutGrid, Lightbulb, Clock, Shield, Settings, Sparkles, TriangleAlert, MessageCircleQuestionMark } from 'lucide-react';
import { useHasPermission } from '@/components/auth/Guard';
import { useXiaobaoWarningRisks } from '@/hooks/useXiaobaoWarningRisks';
import { useWorkspaceWorkItems } from '@/hooks/useWorkspaceWorkItems';
@@ -30,6 +30,7 @@ const NAV_GROUPS = [
{
label: '工作区',
items: [
+ { label: '问翻小宝', path: '/wenfan-xiaobao', icon: MessageCircleQuestionMark, permission: null as string | null },
{ label: '小宝预警', path: '/xiaobao-warning', icon: TriangleAlert, permission: 'xiaobao.warning:view', badge: 'xiaobao-risk' as const },
{ label: '与我相关', path: '/workspace', icon: Inbox, permission: null as string | null, badge: 'workspace-pending' as const },
{ label: '产品', path: '/products', icon: Package, permission: 'product:view' },
diff --git a/apps/web/lib/wenfan-xiaobao-ui.test.ts b/apps/web/lib/wenfan-xiaobao-ui.test.ts
new file mode 100644
index 0000000..8f94086
--- /dev/null
+++ b/apps/web/lib/wenfan-xiaobao-ui.test.ts
@@ -0,0 +1,32 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { existsSync, readFileSync } from 'node:fs';
+import { join } from 'node:path';
+
+test('sidebar links wenfan xiaobao as an independent entry before xiaobao warning', () => {
+ const sidebar = readFileSync(join(process.cwd(), 'components/layout/Sidebar.tsx'), 'utf8');
+
+ const askIndex = sidebar.indexOf("label: '问翻小宝'");
+ const warningIndex = sidebar.indexOf("label: '小宝预警'");
+
+ assert.notEqual(askIndex, -1);
+ assert.notEqual(warningIndex, -1);
+ assert.ok(askIndex < warningIndex);
+ assert.match(sidebar, /path: '\/wenfan-xiaobao'/);
+});
+
+test('wenfan xiaobao page provides records, chat, and voice input surfaces', () => {
+ const pagePath = join(process.cwd(), 'app/wenfan-xiaobao/page.tsx');
+ assert.equal(existsSync(pagePath), true);
+
+ const page = readFileSync(pagePath, 'utf8');
+
+ assert.match(page, /历史记录/);
+ assert.match(page, /对话/);
+ assert.match(page, /textarea/);
+ assert.match(page, /Mic/);
+ assert.match(page, /lg:grid-cols-\[300px_minmax\(0,1fr\)\]/);
+ assert.match(page, /rounded-\[28px\]/);
+ assert.doesNotMatch(page, /: 'border-\[var\(--line\)\] bg-\[var\(--bg\)\]/);
+ assert.match(page, /: 'bg-white hover:bg-\[var\(--bg-subtle\)\]'/);
+});