From dd01d94ade00b32d7a600853ad374e692b82b64f Mon Sep 17 00:00:00 2001 From: selfrelease Date: Mon, 31 Aug 2026 14:11:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=80=81=E6=9D=BF=E5=8F=B02.0=20+=20?= =?UTF-8?q?=E6=89=8B=E6=9C=BA=E7=AB=AF=E9=80=82=E9=85=8D=20+=20=E4=B8=93?= =?UTF-8?q?=E5=AE=B6=E5=BA=93=20+=20=E6=99=BA=E5=9B=8A=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 老板台: AI执行摘要卡 + 待办优先级排序 + 时间线视图 + 环形健康度 - 手机端: 事项看板折叠列表 + 7个页面响应式修复 + 底部安全边距 - 专家库: 新增用户端专家浏览页(参考管理端卡片风格) - 智囊入口: 底部Tab加"智囊" + 4个智囊页场景切换栏 + "我"页分组入口 - FAB弹窗: 场景快速入口 + 指令卡片优化 + 输入区sticky - 后端: /dashboard/summary + /dashboard/timeline API - 后端: life场景事实维度扩充(2→5) + 17个生活关键词优先级 - 前端: 抑制React DevTools startTime噪音错误 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- api.py | 166 ++++++ bossai.py | 32 +- web2/src/App.tsx | 2 + .../components/common/BrainTrustSwitcher.tsx | 42 ++ web2/src/components/common/ChatPanel.tsx | 2 +- web2/src/components/common/ExpertAvatar.tsx | 25 +- .../components/common/QuickCommandPanel.tsx | 205 +++++--- web2/src/components/layout/AppLayout.tsx | 2 +- web2/src/components/layout/Fab.tsx | 4 +- web2/src/lib/api.ts | 4 +- web2/src/lib/nav-config.ts | 4 +- web2/src/main.tsx | 14 + web2/src/pages/Dashboard.tsx | 485 +++++++++++++----- web2/src/pages/Decisions.tsx | 3 + web2/src/pages/DecisionsLog.tsx | 4 +- web2/src/pages/EmotionCenter.tsx | 3 + web2/src/pages/Experts.tsx | 136 +++++ web2/src/pages/KnowledgeBase.tsx | 4 +- web2/src/pages/LegalAdvisor.tsx | 4 + web2/src/pages/LifeAssistant.tsx | 3 + web2/src/pages/Matters.tsx | 95 +++- web2/src/pages/Me.tsx | 50 +- web2/src/pages/Team.tsx | 74 +-- web2/src/types/index.ts | 25 + 24 files changed, 1088 insertions(+), 300 deletions(-) create mode 100644 web2/src/components/common/BrainTrustSwitcher.tsx create mode 100644 web2/src/pages/Experts.tsx diff --git a/api.py b/api.py index a3eeb2d..4791da5 100644 --- a/api.py +++ b/api.py @@ -2410,6 +2410,172 @@ def dashboard_today(authorization: str | None = Header(default=None)): conn.close() +@app.get("/dashboard/summary") +def dashboard_summary(authorization: str | None = Header(default=None)): + """老板台执行摘要:AI 用一句话总结当前状态和建议行动。 + 降级策略:Kimi 不可用时返回规则摘要。 + """ + phone = get_current_phone(authorization) + if not phone: + raise HTTPException(401, "未登录") + conn = _matter_dsn() + try: + cur = conn.cursor() + cur.execute( + "SELECT id, title, risk_level, scene, status FROM matters " + "WHERE user_phone = %s AND status != 'closed' ORDER BY updated_at DESC LIMIT 10", + (phone,), + ) + rows = cur.fetchall() + open_matters = [{"id": r[0], "title": r[1], "risk_level": r[2], "scene": r[3], "status": r[4]} for r in rows] + cur.execute( + "SELECT COUNT(*) FROM matters WHERE user_phone = %s AND status = 'pending_confirm'", + (phone,), + ) + pending_count = cur.fetchone()[0] + cur.execute( + "SELECT COUNT(*) FROM expert_consultations WHERE user_phone = %s AND status = 'replied'", + (phone,), + ) + reply_count = cur.fetchone()[0] + finally: + conn.close() + + red_count = sum(1 for m in open_matters if m["risk_level"] == "red") + orange_count = sum(1 for m in open_matters if m["risk_level"] == "orange") + + # 规则摘要(兜底) + parts = [] + if red_count > 0: + parts.append(f"有 {red_count} 项高风险事项需立即处理") + if orange_count > 0: + parts.append(f"{orange_count} 项中风险事项需关注") + if pending_count > 0: + parts.append(f"{pending_count} 个事项待你确认") + if reply_count > 0: + parts.append(f"{reply_count} 条专家回复待查看") + if not parts: + rule_summary = "当前无待办事项,经营状况良好。可主动发起咨询或查看经营健康度。" + rule_action = "浏览智囊或查看事项看板" + else: + rule_summary = "、".join(parts) + "。" + if red_count > 0: + rule_action = "建议优先处理高风险事项" + elif pending_count > 0: + rule_action = "建议先确认待办事项" + elif reply_count > 0: + rule_action = "建议查看专家回复" + else: + rule_action = "建议关注中风险事项" + + # 尝试 AI 生成更自然的摘要 + try: + if kimi and open_matters: + matter_desc = "\n".join( + f"- {m['title']}(风险:{m['risk_level']},场景:{m['scene']},状态:{m['status']})" + for m in open_matters[:5] + ) + prompt = ( + f"你是中小企业老板的AI助手。请根据以下数据,用一句话(不超过50字)总结当前经营状态," + f"并给出一个最优先的行动建议(不超过20字)。\n\n" + f"待办事项:\n{matter_desc}\n" + f"待确认:{pending_count}个,专家回复:{reply_count}条\n\n" + f"返回 JSON 格式:{{\"summary\": \"...\", \"action\": \"...\"}}" + ) + answer = kimi.chat( + [{"role": "system", "content": "你是经营顾问助手,只返回JSON。"}, + {"role": "user", "content": prompt}], + temperature=0.3, timeout=15, + ) + answer = answer.strip() + if answer.startswith("```"): + answer = answer.split("\n", 1)[1] if "\n" in answer else answer[3:] + if answer.endswith("```"): + answer = answer[:-3] + answer = answer.replace("\u201c", '"').replace("\u201d", '"').strip() + start = answer.find("{") + end = answer.rfind("}") + if start != -1 and end != -1: + import json as _json + parsed = _json.loads(answer[start:end + 1]) + return { + "summary": parsed.get("summary", rule_summary), + "action": parsed.get("action", rule_action), + "ai_generated": True, + } + except Exception: + pass + + return {"summary": rule_summary, "action": rule_action, "ai_generated": False} + + +@app.get("/dashboard/timeline") +def dashboard_timeline(authorization: str | None = Header(default=None)): + """老板台时间线:按时间倒序聚合最近活动(案卡变更、专家回复、风险推送)。""" + phone = get_current_phone(authorization) + if not phone: + raise HTTPException(401, "未登录") + conn = _matter_dsn() + try: + cur = conn.cursor() + items = [] + # 案卡最近更新 + cur.execute( + "SELECT id, title, scene, status, risk_level, updated_at " + "FROM matters WHERE user_phone = %s ORDER BY updated_at DESC LIMIT 8", + (phone,), + ) + for r in cur.fetchall(): + items.append({ + "type": "matter", + "id": r[0], + "title": r[1], + "scene": r[2], + "status": r[3], + "risk_level": r[4], + "time": r[5].isoformat() if r[5] else None, + }) + # 专家回复 + cur.execute( + "SELECT c.id, e.name, e.title, c.reply, c.replied_at " + "FROM expert_consultations c JOIN experts e ON c.expert_id = e.id " + "WHERE c.user_phone = %s AND c.status = 'replied' " + "ORDER BY c.replied_at DESC LIMIT 5", + (phone,), + ) + for r in cur.fetchall(): + items.append({ + "type": "expert_reply", + "id": r[0], + "expert_name": r[1], + "expert_title": r[2], + "snippet": (r[3] or "")[:60], + "time": r[4].isoformat() if r[4] else None, + }) + # 风险推送 + try: + cur.execute( + "SELECT id, title, level, created_at FROM risk_alerts " + "WHERE user_phone = %s ORDER BY created_at DESC LIMIT 5", + (phone,), + ) + for r in cur.fetchall(): + items.append({ + "type": "risk_alert", + "id": r[0], + "title": r[1], + "level": r[2], + "time": r[3].isoformat() if r[3] else None, + }) + except Exception: + pass # risk_alerts 表可能不存在 + # 按时间倒序 + items.sort(key=lambda x: x.get("time") or "", reverse=True) + return {"results": items[:15]} + finally: + conn.close() + + @app.get("/team") def my_team(authorization: str | None = Header(default=None)): """我的团队:咨询过的专家自动沉淀""" diff --git a/bossai.py b/bossai.py index 0053bb1..b117ca9 100644 --- a/bossai.py +++ b/bossai.py @@ -2327,10 +2327,16 @@ class FactFinder: "questions": ["预期利润率大约是多少?", "投资回收期预计多长?"]}, ], "life": [ - {"key": "context", "desc": "具体情境", - "questions": ["能具体描述一下当前的情况吗?", "最希望解决的核心问题是什么?"]}, - {"key": "constraint", "desc": "时间和资源限制", - "questions": ["目前有多少时间可以投入?", "有哪些资源可以利用?"]}, + {"key": "situation", "desc": "具体情境", + "questions": ["这件事具体是怎么发生的?能描述一下经过吗?", "最希望解决的核心问题是什么?"]}, + {"key": "people", "desc": "涉及的人", + "questions": ["这件事涉及哪些人?他们的态度如何?", "对方目前是什么反应?"]}, + {"key": "duration", "desc": "持续时间和频率", + "questions": ["这种情况持续多久了?是偶尔发生还是经常出现?", "上次类似情况是什么时候?"]}, + {"key": "attempt", "desc": "已尝试的解决方式", + "questions": ["之前有没有尝试过什么方法来解决?", "效果如何?"]}, + {"key": "goal", "desc": "期望的结果", + "questions": ["你期望达到什么样的结果?", "最理想的状态是什么?"]}, ], "emotion": [ {"key": "feeling", "desc": "当前情绪状态", @@ -2388,6 +2394,24 @@ class FactFinder: "压力": ["feeling", "trigger"], "焦虑": ["feeling", "trigger"], "失眠": ["feeling", "trigger"], + # 生活场景关键词 + "夫妻": ["situation", "people", "attempt"], + "吵架": ["situation", "people", "duration"], + "争吵": ["situation", "people", "duration"], + "婚姻": ["situation", "people", "attempt"], + "亲子": ["situation", "people", "duration"], + "孩子": ["situation", "people", "goal"], + "教育": ["situation", "goal", "people"], + "升学": ["situation", "goal", "attempt"], + "养老": ["situation", "people", "attempt"], + "父母": ["situation", "people", "duration"], + "健康": ["situation", "duration", "attempt"], + "体检": ["situation", "goal", "attempt"], + "睡眠": ["situation", "duration", "attempt"], + "出差": ["situation", "duration", "goal"], + "出行": ["situation", "duration", "goal"], + "饮食": ["situation", "duration", "attempt"], + "运动": ["situation", "duration", "goal"], } def __init__(self, max_rounds: int = 2, max_questions_per_round: int = 3): diff --git a/web2/src/App.tsx b/web2/src/App.tsx index 3b94adf..716e3f8 100644 --- a/web2/src/App.tsx +++ b/web2/src/App.tsx @@ -7,6 +7,7 @@ import { Decisions } from '@/pages/Decisions' import { LifeAssistant } from '@/pages/LifeAssistant' import { EmotionCenter } from '@/pages/EmotionCenter' import { Team } from '@/pages/Team' +import { Experts } from '@/pages/Experts' import { DecisionsLog } from '@/pages/DecisionsLog' import { KnowledgeBase } from '@/pages/KnowledgeBase' import { ChatHistory } from '@/pages/ChatHistory' @@ -41,6 +42,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/web2/src/components/common/BrainTrustSwitcher.tsx b/web2/src/components/common/BrainTrustSwitcher.tsx new file mode 100644 index 0000000..95ea14e --- /dev/null +++ b/web2/src/components/common/BrainTrustSwitcher.tsx @@ -0,0 +1,42 @@ +/** 智囊场景切换栏(法务/决策/生活/情绪) + * 在 4 个智囊页面顶部展示,点击跳转到对应场景 + */ +import { useNavigate, useLocation } from 'react-router-dom' +import { Scale, TrendingUp, Heart, ShieldCheck } from 'lucide-react' +import { cn } from '@/lib/utils' + +const SCENES = [ + { path: '/legal', label: '法务', icon: Scale }, + { path: '/decisions', label: '决策', icon: TrendingUp }, + { path: '/life', label: '生活', icon: Heart }, + { path: '/emotion', label: '情绪', icon: ShieldCheck }, +] + +export function BrainTrustSwitcher() { + const navigate = useNavigate() + const location = useLocation() + + return ( +
+ {SCENES.map((s) => { + const Icon = s.icon + const active = location.pathname === s.path + return ( + + ) + })} +
+ ) +} diff --git a/web2/src/components/common/ChatPanel.tsx b/web2/src/components/common/ChatPanel.tsx index 9b26863..0e88888 100644 --- a/web2/src/components/common/ChatPanel.tsx +++ b/web2/src/components/common/ChatPanel.tsx @@ -907,7 +907,7 @@ export function ChatPanel({
对话 {/* 模式选择:紧凑 segmented control */} -
+
{modes.map((m) => (
-
- {/* 画像不完整提示 */} - {commands?.profile_incomplete && commands.incomplete_hint && ( -
- {commands.incomplete_hint} -
- )} - - {loading ? ( -
加载中...
- ) : ( - <> - {/* 指令分组 */} - {sections.map(({ key, label }) => { - const items = (commands?.[key as keyof QuickCommands] as Array<{ id: string; label: string; template: string }>) || [] - if (items.length === 0) return null - const Icon = SCENE_ICONS[key] || Scale - return ( -
-
- - {label} -
-
- {items.map((cmd) => ( - - ))} -
+ {/* 场景快速入口 */} +
+
+ {QUICK_ENTRIES.map((entry) => { + const Icon = entry.icon + return ( + + ) + })} +
+
- {/* 文字/语音输入 */} -
-
- + {/* 画像不完整提示 */} + {commands?.profile_incomplete && commands.incomplete_hint && ( +
+ {commands.incomplete_hint} +
+ )} + + {/* 指令分组 */} +
+ {loading ? ( +
+ + AI 正在生成个性化指令... +
+ ) : ( + sections.map(({ key, label }) => { + const items = (commands?.[key as keyof QuickCommands] as Array<{ id: string; label: string; template: string }>) || [] + if (items.length === 0) return null + const Icon = SCENE_ICONS[key] || Scale + return ( +
+
+ + {label} +
+
+ {items.map((cmd) => ( + + ))} +
+
+ ) + }) + )} +
+ + {/* 底部输入区 */} +
+
+ +
setTextInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleTextSubmit()} placeholder={listening ? '正在聆听...' : '或直接输入你的问题...'} - className="flex-1 rounded-lg border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" + className="w-full rounded-full border border-border bg-background px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30" /> -
- {listening && ( -

点击麦克风停止

- )} +
+ {listening && ( +

点击麦克风停止录音

+ )}
diff --git a/web2/src/components/layout/AppLayout.tsx b/web2/src/components/layout/AppLayout.tsx index b52688a..535b08e 100644 --- a/web2/src/components/layout/AppLayout.tsx +++ b/web2/src/components/layout/AppLayout.tsx @@ -17,7 +17,7 @@ export function AppLayout() {
-
+
diff --git a/web2/src/components/layout/Fab.tsx b/web2/src/components/layout/Fab.tsx index f062704..6be8663 100644 --- a/web2/src/components/layout/Fab.tsx +++ b/web2/src/components/layout/Fab.tsx @@ -11,10 +11,10 @@ export function Fab() { <> setOpen(false)} /> diff --git a/web2/src/lib/api.ts b/web2/src/lib/api.ts index b6a4a00..20d1012 100644 --- a/web2/src/lib/api.ts +++ b/web2/src/lib/api.ts @@ -6,7 +6,7 @@ import type { AskResponse, EmotionMode, Expert, ExpertService, Consultation, FileInspectResponse, HistoryMessage, HistorySession, KnowledgeDoc, Scene, SessionMode, SessionResponse, Settings, - Matter, MatterBoard, DashboardToday, RiskAlert, + Matter, MatterBoard, DashboardToday, DashboardSummary, TimelineItem, RiskAlert, ExpertRelation, QuickCommands, } from '@/types' @@ -350,6 +350,8 @@ export const api = { // ===== 2.0 新增:老板台 ===== getDashboardToday: () => request('/dashboard/today'), + getDashboardSummary: () => request('/dashboard/summary'), + getDashboardTimeline: () => request<{ results: TimelineItem[] }>('/dashboard/timeline'), // ===== 2.0 新增:我的团队 ===== getTeam: () => request<{ results: ExpertRelation[] }>('/team'), diff --git a/web2/src/lib/nav-config.ts b/web2/src/lib/nav-config.ts index a1797f7..1daa2f2 100644 --- a/web2/src/lib/nav-config.ts +++ b/web2/src/lib/nav-config.ts @@ -30,12 +30,13 @@ export const USER_NAV: NavItem[] = [ // 我的事项 { path: '/matters', label: '事项看板', icon: ClipboardList, group: '我的事项', tab: true }, // 智囊 - { path: '/legal', label: '法务参谋', icon: Scale, group: '智囊' }, + { path: '/legal', label: '智囊', icon: Scale, group: '智囊', tab: true }, { path: '/decisions', label: '经营决策', icon: TrendingUp, group: '智囊' }, { path: '/life', label: '生活助理', icon: Heart, group: '智囊' }, { path: '/emotion', label: '情绪中心', icon: ShieldCheck, group: '智囊' }, // 我的资源 { path: '/team', label: '我的团队', icon: Users, group: '我的资源', tab: true }, + { path: '/experts', label: '专家库', icon: Users, group: '我的资源' }, { path: '/decisions-log', label: '决策日志', icon: FileText, group: '我的资源' }, { path: '/knowledge', label: '知识库', icon: BookOpen, group: '我的资源' }, { path: '/history', label: '对话历史', icon: History, group: '我的资源' }, @@ -96,6 +97,7 @@ export const HEADER_TITLES: Record = { '/life': '生活助理', '/emotion': '情绪中心', '/team': '我的团队', + '/experts': '专家库', '/decisions-log': '决策日志', '/knowledge': '知识库', '/history': '对话历史', diff --git a/web2/src/main.tsx b/web2/src/main.tsx index 4e55d7d..ac30079 100644 --- a/web2/src/main.tsx +++ b/web2/src/main.tsx @@ -4,6 +4,20 @@ import { BrowserRouter } from 'react-router-dom' import App from './App' import './index.css' +/** 抑制 React DevTools 扩展的 startTime 兼容性噪音错误(非应用代码问题) */ +const originalError = window.console.error +window.console.error = (...args: unknown[]) => { + const msg = String(args[0] || '') + if (msg.includes('startTime') || msg.includes('React DevTools')) return + originalError.call(console, ...args) +} +window.addEventListener('error', (e) => { + if (e.message?.includes('startTime') || e.message?.includes('reportAllChanges')) { + e.preventDefault() + return true + } +}) + createRoot(document.getElementById('root')!).render( diff --git a/web2/src/pages/Dashboard.tsx b/web2/src/pages/Dashboard.tsx index 63cb3f2..641811f 100644 --- a/web2/src/pages/Dashboard.tsx +++ b/web2/src/pages/Dashboard.tsx @@ -1,28 +1,133 @@ -/** 老板台(TASK-007 + TASK-024) - * 替代 v1.0 Dashboard,展示"今日要事": - * - 问候语 - * - 风险推送(AI 主动提醒) - * - 高风险提醒 - * - 待确认事项 - * - 专家回复 - * - 经营健康度 +/** 老板台 2.0(TASK-007 + TASK-024) + * 老板视角仪表盘: + * - 执行摘要卡(AI 生成今日总结 + 建议行动) + * - 概览数字(风险/待确认/回复/健康度) + * - 待办优先级(按紧急程度排序,最紧急的置顶放大) + * - 时间线(最近活动按时间倒序) + * - 快捷入口 + * - 经营健康度(环形) */ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { AlertTriangle, ClipboardCheck, MessageSquare, Activity, ArrowRight, Radar } from 'lucide-react' +import { + ClipboardCheck, MessageSquare, Activity, ArrowRight, + Radar, ShieldCheck, Scale, TrendingUp, Heart, Plus, ChevronRight, + Sparkles, Clock, CircleAlert, +} from 'lucide-react' import { PageLayout } from '@/components/common/PageLayout' import { Card, CardContent } from '@/components/ui/card' import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' import { LoadingState } from '@/components/common/LoadingState' import { ErrorState } from '@/components/common/ErrorState' import { EmptyState } from '@/components/common/EmptyState' import { api } from '@/lib/api' -import { greeting, formatRelative, SCENE_LABELS, RISK_LABELS } from '@/lib/utils' -import type { DashboardToday, RiskAlert } from '@/types' +import { greeting, formatRelative, SCENE_LABELS, RISK_LABELS, MATTER_STATUS_LABELS, cn } from '@/lib/utils' +import type { DashboardToday, DashboardSummary, TimelineItem, RiskAlert, Scene } from '@/types' + +/** 环形进度条 */ +function RingProgress({ value, label, color }: { value: number; label: string; color: string }) { + const radius = 28 + const circumference = 2 * Math.PI * radius + const offset = circumference - (value / 100) * circumference + return ( +
+
+ + + + +
+ {value} +
+
+ {label} +
+ ) +} + +const SCENE_ICONS: Record = { + legal: Scale, + decision: TrendingUp, + life: Heart, + emotion: ShieldCheck, +} + +const QUICK_ACTIONS = [ + { label: '法务咨询', icon: Scale, path: '/legal', color: 'text-blue-600 bg-blue-50' }, + { label: '决策参谋', icon: TrendingUp, path: '/decisions', color: 'text-violet-600 bg-violet-50' }, + { label: '事项看板', icon: ClipboardCheck, path: '/matters', color: 'text-amber-600 bg-amber-50' }, + { label: '我的团队', icon: MessageSquare, path: '/team', color: 'text-emerald-600 bg-emerald-50' }, +] + +/** 待办优先级定义 */ +const PRIORITY_CONFIG = { + red: { label: '紧急', color: 'text-red-600 bg-red-50 border-red-200', rank: 0 }, + orange: { label: '高优', color: 'text-orange-600 bg-orange-50 border-orange-200', rank: 1 }, + pending: { label: '待确认', color: 'text-indigo-600 bg-indigo-50 border-indigo-200', rank: 2 }, + reply: { label: '待查看', color: 'text-emerald-600 bg-emerald-50 border-emerald-200', rank: 3 }, + alert: { label: '提醒', color: 'text-amber-600 bg-amber-50 border-amber-200', rank: 4 }, +} + +/** 时间线条目渲染 */ +function TimelineEntry({ item, onClick }: { item: TimelineItem; onClick: () => void }) { + if (item.type === 'matter') { + const Icon = SCENE_ICONS[item.scene || 'legal'] || Scale + return ( + + ) + } + if (item.type === 'expert_reply') { + return ( + + ) + } + // risk_alert + return ( + + ) +} export function Dashboard() { const navigate = useNavigate() const [data, setData] = useState(null) + const [summary, setSummary] = useState(null) + const [timeline, setTimeline] = useState([]) const [alerts, setAlerts] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') @@ -31,9 +136,14 @@ export function Dashboard() { setLoading(true); setError('') Promise.all([ api.getDashboardToday(), + api.getDashboardSummary().catch(() => null), + api.getDashboardTimeline().catch(() => ({ results: [] as TimelineItem[] })), api.getRiskAlerts(true).catch(() => ({ results: [] as RiskAlert[] })), ]) - .then(([d, a]) => { setData(d); setAlerts(a.results); setLoading(false) }) + .then(([d, s, t, a]) => { + setData(d); setSummary(s); setTimeline(t.results); setAlerts(a.results) + setLoading(false) + }) .catch((e) => { setError(e.message); setLoading(false) }) } @@ -50,124 +160,248 @@ export function Dashboard() { if (error) return if (!data) return - const hasContent = data.high_risks.length > 0 || data.pending_confirm.length > 0 || data.expert_replies.length > 0 + const riskCount = alerts.length + data.high_risks.length + const pendingCount = data.pending_confirm.length + const replyCount = data.expert_replies.length + const hasContent = riskCount > 0 || pendingCount > 0 || replyCount > 0 + + /** 构建待办优先级列表 */ + const todos: Array<{ + id: string + title: string + desc: string + priority: keyof typeof PRIORITY_CONFIG + action: () => void + actionLabel: string + }> = [] + + // 高风险(红色) + data.high_risks.filter((r) => r.level === 'red').forEach((r) => { + todos.push({ + id: `risk-${r.matter_id}`, title: r.title, desc: `${SCENE_LABELS[r.scene]} · ${RISK_LABELS[r.level]}`, + priority: 'red', action: () => navigate('/matters'), actionLabel: '立即处理', + }) + }) + // 风险推送 + alerts.forEach((a) => { + todos.push({ + id: `alert-${a.id}`, title: a.title, desc: a.content.slice(0, 60), + priority: a.level === 'red' ? 'red' : 'alert', + action: () => handleReadAlert(a.id), actionLabel: '稍后', + }) + }) + // 中风险(橙色) + data.high_risks.filter((r) => r.level === 'orange').forEach((r) => { + todos.push({ + id: `orange-${r.matter_id}`, title: r.title, desc: `${SCENE_LABELS[r.scene]} · ${RISK_LABELS[r.level]}`, + priority: 'orange', action: () => navigate('/matters'), actionLabel: '去处理', + }) + }) + // 待确认 + data.pending_confirm.forEach((p) => { + todos.push({ + id: `pending-${p.matter_id}`, title: p.title, desc: `${SCENE_LABELS[p.scene]} · ${formatRelative(p.updated_at)}`, + priority: 'pending', action: () => navigate('/matters'), actionLabel: '去确认', + }) + }) + // 专家回复 + data.expert_replies.forEach((r) => { + todos.push({ + id: `reply-${r.consultation_id}`, title: `${r.expert_name} · ${r.expert_title}`, + desc: r.snippet, priority: 'reply', action: () => navigate('/team'), actionLabel: '查看', + }) + }) + + // 按优先级排序 + todos.sort((a, b) => PRIORITY_CONFIG[a.priority].rank - PRIORITY_CONFIG[b.priority].rank) return ( - {!hasContent && ( + {/* 执行摘要卡 */} + {summary && ( + + +
+
+ +
+
+
+ 今日要点 + {summary.ai_generated && ( + AI + )} +
+

{summary.summary}

+
+ + {summary.action} +
+
+
+
+
+ )} + + {/* 概览数字 */} +
+ + + + + + +
+
+ + 健康度 +
+

+ {Math.round((data.health_score.legal + data.health_score.operation + data.health_score.compliance) / 3)} +

+
+
+ + {/* 快捷入口 */} +
+ {QUICK_ACTIONS.map((a) => { + const Icon = a.icon + return ( + + ) + })} +
+ + {!hasContent && riskCount === 0 && timeline.length === 0 && ( navigate('/legal')}>开始咨询} + action={} /> )} -
- {/* 风险推送(AI 主动提醒) */} - {alerts.length > 0 && ( - - -
- - {alerts.length} 条风险提醒 -
-
- {alerts.map((a) => ( -
-
-
- - {a.title} + {/* 待办优先级列表 */} + {todos.length > 0 && ( + + +
+ + 待办优先级 + {todos.length} +
+
+ {todos.map((todo, idx) => { + const config = PRIORITY_CONFIG[todo.priority] + const isTop = idx === 0 + return ( +
+
+ {/* 优先级标记 */} +
+ {config.label} + {isTop && 最紧急}
-

{a.content}

-
-
- - -
-
- ))} -
-
-
- )} - - {/* 高风险提醒 */} - {data.high_risks.length > 0 && ( - - -
- - {data.high_risks.length} 条高风险提醒 -
-
- {data.high_risks.map((r) => ( -
-
-
- {r.title} - {RISK_LABELS[r.level]} -
- {SCENE_LABELS[r.scene]} -
- -
- ))} -
-
-
- )} - - {/* 待确认事项 */} - {data.pending_confirm.length > 0 && ( - - -
- - {data.pending_confirm.length} 个事项待你确认 -
-
- {data.pending_confirm.map((p) => ( -
-
-
{p.title}
-
- {SCENE_LABELS[p.scene]} · {formatRelative(p.updated_at)} +
+
+ {todo.title} +
+
{todo.desc}
-
- ))} -
- - - )} + ) + })} +
+
+
+ )} - {/* 专家回复 */} - {data.expert_replies.length > 0 && ( - + {/* 时间线 + 健康度(两列) */} +
+ {/* 时间线(占 2 列) */} + {timeline.length > 0 && ( + -
- - {data.expert_replies.length} 条专家回复 +
+ + 最近动态
-
- {data.expert_replies.map((r) => ( -
-
-
{r.expert_name} · {r.expert_title}
-
{r.snippet}
-
{formatRelative(r.replied_at)}
-
- -
+
+ {timeline.map((item) => ( + { + if (item.type === 'matter') navigate('/matters') + else if (item.type === 'expert_reply') navigate('/team') + else navigate('/knowledge') + }} + /> ))}
@@ -177,29 +411,14 @@ export function Dashboard() { {/* 经营健康度 */} -
+
- 你的经营健康度 + 经营健康度
-
- {[ - { label: '法务', value: data.health_score.legal }, - { label: '经营', value: data.health_score.operation }, - { label: '合规', value: data.health_score.compliance }, - ].map((s) => ( -
-
- {s.label} - {s.value}% -
-
-
= 70 ? 'bg-emerald-500' : s.value >= 40 ? 'bg-amber-500' : 'bg-red-500'}`} - style={{ width: `${s.value}%` }} - /> -
-
- ))} +
+ + +
diff --git a/web2/src/pages/Decisions.tsx b/web2/src/pages/Decisions.tsx index 876455d..c212309 100644 --- a/web2/src/pages/Decisions.tsx +++ b/web2/src/pages/Decisions.tsx @@ -4,6 +4,7 @@ import { useState, useCallback } from 'react' import { PageLayout } from '@/components/common/PageLayout' import { ChatPanel } from '@/components/common/ChatPanel' +import { BrainTrustSwitcher } from '@/components/common/BrainTrustSwitcher' import { Card, CardContent, CardHeader } from '@/components/ui/card' import { TrendingUp, ChevronDown, RefreshCw, Loader2 } from 'lucide-react' import { cn } from '@/lib/utils' @@ -55,6 +56,8 @@ export function Decisions() { title="经营决策助手" description="项目承接、扩张收缩、招人、降价、业务线调整、融资合伙。情景推演,把利弊讲透。" > + +
diff --git a/web2/src/pages/DecisionsLog.tsx b/web2/src/pages/DecisionsLog.tsx index 1543d0c..d0bcfc8 100644 --- a/web2/src/pages/DecisionsLog.tsx +++ b/web2/src/pages/DecisionsLog.tsx @@ -125,8 +125,8 @@ export function DecisionsLog() {
-
- {matter.title} +
+ {matter.title} {SCENE_LABELS[matter.scene]} {matter.risk_level && ( diff --git a/web2/src/pages/EmotionCenter.tsx b/web2/src/pages/EmotionCenter.tsx index 9c98c89..8bf2566 100644 --- a/web2/src/pages/EmotionCenter.tsx +++ b/web2/src/pages/EmotionCenter.tsx @@ -5,6 +5,7 @@ import { useState } from 'react' import { Brain, HeartHandshake, Zap, Phone, ChevronDown } from 'lucide-react' import { PageLayout } from '@/components/common/PageLayout' import { ChatPanel } from '@/components/common/ChatPanel' +import { BrainTrustSwitcher } from '@/components/common/BrainTrustSwitcher' import { Card, CardContent, CardHeader } from '@/components/ui/card' import { cn } from '@/lib/utils' import type { EmotionMode } from '@/types' @@ -58,6 +59,8 @@ export function EmotionCenter() { title="情绪价值引擎" description="先处理情绪,再处理事情。选择适合你当前状态的模式。" > + + {/* 危机干预提示 */} diff --git a/web2/src/pages/Experts.tsx b/web2/src/pages/Experts.tsx new file mode 100644 index 0000000..7d678cc --- /dev/null +++ b/web2/src/pages/Experts.tsx @@ -0,0 +1,136 @@ +/** 专家库:浏览所有专家,选择后跳转到对应智囊场景咨询 + * 卡片样式参考管理端专家管理:长方形头像 + 姓名/评分 + 简介 + 专长标签 + */ +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { Users, Star, ChevronRight, Briefcase } from 'lucide-react' +import { PageLayout } from '@/components/common/PageLayout' +import { Card, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { LoadingState } from '@/components/common/LoadingState' +import { ErrorState } from '@/components/common/ErrorState' +import { EmptyState } from '@/components/common/EmptyState' +import { ExpertAvatar } from '@/components/common/ExpertAvatar' +import { api } from '@/lib/api' +import { SCENE_LABELS, cn } from '@/lib/utils' +import type { Expert, Scene } from '@/types' + +const CATEGORY_FILTERS: { value: Scene | 'all'; label: string }[] = [ + { value: 'all', label: '全部' }, + { value: 'legal', label: '法务' }, + { value: 'decision', label: '决策' }, + { value: 'life', label: '生活' }, + { value: 'emotion', label: '情绪' }, +] + +export function Experts() { + const navigate = useNavigate() + const [experts, setExperts] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [filter, setFilter] = useState('all') + + const load = () => { + setLoading(true); setError('') + api.listExperts(filter === 'all' ? undefined : filter) + .then((r) => { setExperts(r.results); setLoading(false) }) + .catch((e) => { setError(e.message); setLoading(false) }) + } + + useEffect(load, [filter]) + + if (loading) return + if (error) return + + return ( + + {/* 分类筛选 */} +
+ {CATEGORY_FILTERS.map((f) => ( + + ))} +
+ + {experts.length === 0 ? ( + + ) : ( +
+ {experts.map((ex) => { + const stars = (ex.rate / 100).toFixed(1) + return ( + + + {/* 头部:长方形头像(左) + 姓名/评分(右) */} +
+ +
+
+

{ex.name}

+
+

{ex.title}

+ {ex.rating_count > 0 && ( +
+ + {stars} + ({ex.rating_count}人评价) +
+ )} + {/* 简介 */} + {ex.bio && ( +

{ex.bio}

+ )} + {/* 专长标签 */} + {ex.expertise && ( +
+ {ex.expertise.split(',').map((tag, i) => ( + {tag.trim()} + ))} +
+ )} +
+
+ + {/* 底部:分类 + 咨询按钮 */} +
+ + {SCENE_LABELS[ex.category] || ex.category} + + +
+
+
+ ) + })} +
+ )} +
+ ) +} diff --git a/web2/src/pages/KnowledgeBase.tsx b/web2/src/pages/KnowledgeBase.tsx index 77afea1..a9776ff 100644 --- a/web2/src/pages/KnowledgeBase.tsx +++ b/web2/src/pages/KnowledgeBase.tsx @@ -303,9 +303,9 @@ export function KnowledgeBase() {
-
+
{typeInfo && } -

{doc.title}

+

{doc.title}

{doc.doc_type && ( {typeInfo?.label || doc.doc_type} )} diff --git a/web2/src/pages/LegalAdvisor.tsx b/web2/src/pages/LegalAdvisor.tsx index 01669a7..95fb38a 100644 --- a/web2/src/pages/LegalAdvisor.tsx +++ b/web2/src/pages/LegalAdvisor.tsx @@ -1,9 +1,11 @@ /** 法务参谋页:话题选择 + 换一批 + ChatPanel * scene='legal', 默认保密模式, 显示风险卡, 启用上传 + * 顶部含智囊场景切换栏(法务/决策/生活/情绪) */ import { useState, useCallback } from 'react' import { PageLayout } from '@/components/common/PageLayout' import { ChatPanel } from '@/components/common/ChatPanel' +import { BrainTrustSwitcher } from '@/components/common/BrainTrustSwitcher' import { Card, CardContent, CardHeader } from '@/components/ui/card' import { Briefcase, ChevronDown, RefreshCw, Loader2 } from 'lucide-react' import { cn } from '@/lib/utils' @@ -56,6 +58,8 @@ export function LegalAdvisor() { title="法务参谋" description="劳动用工、合同审查、债权债务、股东纠纷、税务风险、诉讼仲裁。AI 输出不替代执业律师正式意见。" > + +
diff --git a/web2/src/pages/LifeAssistant.tsx b/web2/src/pages/LifeAssistant.tsx index 5cf105e..14082c2 100644 --- a/web2/src/pages/LifeAssistant.tsx +++ b/web2/src/pages/LifeAssistant.tsx @@ -4,6 +4,7 @@ import { useState, useCallback } from 'react' import { PageLayout } from '@/components/common/PageLayout' import { ChatPanel } from '@/components/common/ChatPanel' +import { BrainTrustSwitcher } from '@/components/common/BrainTrustSwitcher' import { Card, CardContent, CardHeader } from '@/components/ui/card' import { Heart, ChevronDown, RefreshCw, Loader2 } from 'lucide-react' import { cn } from '@/lib/utils' @@ -55,6 +56,8 @@ export function LifeAssistant() { title="生活助理" description="家庭沟通、子女教育、父母养老、健康习惯、出行规划。老板的生活问题,也值得认真对待。" > + +
diff --git a/web2/src/pages/Matters.tsx b/web2/src/pages/Matters.tsx index 1a2aaef..1f06ad8 100644 --- a/web2/src/pages/Matters.tsx +++ b/web2/src/pages/Matters.tsx @@ -1,10 +1,10 @@ /** 事项看板(TASK-008) * 案卡按状态分列展示,点击查看详情(弹窗),支持断点续接 - * 移动端横向滚动看板列 + * 桌面端:6 列看板;移动端:按状态折叠列表 */ import { useEffect, useState, useCallback } from 'react' import { useNavigate } from 'react-router-dom' -import { ClipboardList, Plus, Scale, TrendingUp, Heart, ShieldCheck, ArrowRight, Loader2 } from 'lucide-react' +import { ClipboardList, Plus, Scale, TrendingUp, Heart, ShieldCheck, ArrowRight, Loader2, ChevronDown } from 'lucide-react' import { PageLayout } from '@/components/common/PageLayout' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' @@ -44,11 +44,19 @@ export function Matters() { const [error, setError] = useState('') const [selectedMatter, setSelectedMatter] = useState(null) const [detailLoading, setDetailLoading] = useState(false) + /** 移动端折叠状态:默认展开有内容的列 */ + const [expandedCols, setExpandedCols] = useState>(new Set()) const load = useCallback(() => { setLoading(true); setError('') api.getMatters() - .then((r) => { setBoard(r.board); setTotal(r.total); setLoading(false) }) + .then((r) => { + setBoard(r.board); setTotal(r.total); setLoading(false) + // 移动端:默认展开有内容的列 + const expanded = new Set() + COLUMNS.forEach((s) => { if (r.board[s]?.length > 0) expanded.add(s) }) + setExpandedCols(expanded) + }) .catch((e) => { setError(e.message); setLoading(false) }) }, []) @@ -96,28 +104,69 @@ export function Matters() { action={} /> ) : ( -
- {COLUMNS.map((status) => { - const items = board?.[status] || [] - return ( -
-
- {MATTER_STATUS_LABELS[status]} - {items.length} -
-
- {items.length === 0 ? ( -
暂无
- ) : ( - items.map((m) => ( - handleCardClick(m)} /> - )) + <> + {/* 移动端:按状态折叠列表 */} +
+ {COLUMNS.map((status) => { + const items = board?.[status] || [] + if (items.length === 0) return null + const expanded = expandedCols.has(status) + return ( +
+ + {expanded && ( +
+ {items.map((m) => ( + handleCardClick(m)} /> + ))} +
)}
-
- ) - })} -
+ ) + })} +
+ + {/* 桌面端:6 列看板 */} +
+ {COLUMNS.map((status) => { + const items = board?.[status] || [] + return ( +
+
+ {MATTER_STATUS_LABELS[status]} + {items.length} +
+
+ {items.length === 0 ? ( +
暂无
+ ) : ( + items.map((m) => ( + handleCardClick(m)} /> + )) + )} +
+
+ ) + })} +
+ )} {/* 案卡详情弹窗 */} diff --git a/web2/src/pages/Me.tsx b/web2/src/pages/Me.tsx index 8c89b25..7c8acba 100644 --- a/web2/src/pages/Me.tsx +++ b/web2/src/pages/Me.tsx @@ -1,11 +1,23 @@ /** 移动端"我"聚合页:头像占位+入口列表 */ import { NavLink } from 'react-router-dom' -import { User, History, BookOpen, Settings as SettingsIcon, ChevronRight } from 'lucide-react' +import { + User, History, BookOpen, Settings as SettingsIcon, ChevronRight, + Scale, TrendingUp, Heart, ShieldCheck, FileText, Users, +} from 'lucide-react' import { getName } from '@/lib/auth' export function Me() { const name = getName() - const items = [ + const brainTrust = [ + { path: '/legal', label: '法务参谋', icon: Scale }, + { path: '/decisions', label: '经营决策', icon: TrendingUp }, + { path: '/life', label: '生活助理', icon: Heart }, + { path: '/emotion', label: '情绪中心', icon: ShieldCheck }, + ] + const resources = [ + { path: '/team', label: '我的团队', icon: Users }, + { path: '/experts', label: '专家库', icon: Users }, + { path: '/decisions-log', label: '决策日志', icon: FileText }, { path: '/profile', label: '我的画像', icon: User }, { path: '/history', label: '对话历史', icon: History }, { path: '/knowledge', label: '知识库', icon: BookOpen }, @@ -22,14 +34,32 @@ export function Me() {
中小企业老板
-
- {items.map((item) => ( - - - {item.label} - - - ))} + + {/* 智囊 */} +
+

智囊

+
+ {brainTrust.map((item) => ( + + + {item.label} + + ))} +
+
+ + {/* 我的资源 */} +
+

我的资源

+
+ {resources.map((item) => ( + + + {item.label} + + + ))} +
) diff --git a/web2/src/pages/Team.tsx b/web2/src/pages/Team.tsx index f654518..a8d74f9 100644 --- a/web2/src/pages/Team.tsx +++ b/web2/src/pages/Team.tsx @@ -1,5 +1,6 @@ /** 我的团队(TASK-017) * 重构专家库为关系视图:咨询过的专家自动沉淀 + * 手机端:紧凑卡片布局,信息分层展示 */ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' @@ -7,6 +8,7 @@ import { Users, MessageSquare, Plus } from 'lucide-react' import { PageLayout } from '@/components/common/PageLayout' import { Card, CardContent } from '@/components/ui/card' import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' import { LoadingState } from '@/components/common/LoadingState' import { ErrorState } from '@/components/common/ErrorState' import { EmptyState } from '@/components/common/EmptyState' @@ -37,53 +39,57 @@ export function Team() { navigate('/legal')}> 找新专家} + actions={} > {team.length === 0 ? ( navigate('/legal')}>开始咨询} + action={} /> ) : (
{team.map((member) => ( - - -
-
- {member.name} - {member.title} - {member.online && ( - - 在线 - - )} -
-
- {SCENE_LABELS[member.category] || member.category} - · - 咨询 {member.consultation_count} 次 - {member.last_consulted_at && ( - <> - · - 最近 {formatRelative(member.last_consulted_at)} - - )} -
- {member.last_topic && ( -
- 上次话题: {member.last_topic} + + {/* 上层:头像 + 名称 + 在线状态 */} +
+ +
+
+ {member.name} + {member.title} + {member.online && ( + + 在线 + + )}
- )} + {/* 下层:标签信息 */} +
+ + {SCENE_LABELS[member.category] || member.category} + + 咨询 {member.consultation_count} 次 + {member.last_consulted_at && ( + · 最近 {formatRelative(member.last_consulted_at)} + )} +
+
-
+ + {/* 上次话题 + 操作按钮 */} + {member.last_topic && ( +
+ 上次话题: {member.last_topic} +
+ )} +