refactor: 智囊三场景去掉话题选择层,直接展示一批问题可点击开问
CI / 后端测试 (push) Has been cancelled
CI / 前端构建 (push) Has been cancelled

This commit is contained in:
selfrelease
2026-08-31 16:44:48 +08:00
parent 0a5b593cb4
commit 35cc6e54ea
3 changed files with 73 additions and 328 deletions
+21 -106
View File
@@ -1,127 +1,42 @@
/** 经营决策页:话题选择 + 换一批 + ChatPanel /** 经营决策页:直接展示一批问题,点击即问
* scene='decision', 默认普通模式, 不显示风险卡 * scene='decision', 默认普通模式, 不显示风险卡
*/ */
import { useState, useCallback } from 'react'
import { PageLayout } from '@/components/common/PageLayout' import { PageLayout } from '@/components/common/PageLayout'
import { ChatPanel } from '@/components/common/ChatPanel' import { ChatPanel } from '@/components/common/ChatPanel'
import { BrainTrustSwitcher } from '@/components/common/BrainTrustSwitcher' 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'
import { api } from '@/lib/api'
/** 话题类型 */ /** 初始兜底示例问题(覆盖扩张/承接/定价/招人/融资/转型高频场景) */
interface Topic { const FALLBACK_SAMPLES: string[] = [
title: string '现在适合扩张开第二家店吗?',
desc: string '是否应该接付款周期90天的大单?',
samples: string[] '该不该降价15%抢竞品客户?',
} '现在该招人还是裁人?',
'现在适合融资吗?',
/** 经营决策助手:初始兜底话题,换一批时 AI 动态生成 */ '业务转型方向怎么选?',
const FALLBACK_TOPICS: Topic[] = [ '要不要关闭亏损的业务线?',
{ title: '扩张收缩', desc: '开店、关店、业务线', samples: ['现在适合扩张开第二家店吗?', '要不要关闭亏损的业务线?', '新业务线该自营还是加盟?', '应该先扩张还是先巩固?'] }, '长期框架合同要不要签?',
{ title: '项目承接', desc: '大单、长期合同', samples: ['是否应该接付款周期90天的大单?', '长期框架合同要不要签?', '预付款比例低的项目能接吗?', '客户要求独家合作利弊?'] }, '产品涨价怎么跟客户沟通?',
{ title: '定价策略', desc: '涨价、降价、促销', samples: ['该不该降价15%抢竞品客户?', '产品涨价怎么跟客户沟通?', '促销活动会不会损害品牌?', '成本上涨如何调整定价?'] }, '关键岗位招人还是培养?',
{ title: '招人裁员', desc: '招聘、解雇、绩效', samples: ['现在该招人还是裁人?', '裁掉绩效差的员工怎么操作?', '关键岗位招人还是培养?', '外包和正式员工怎么选?'] }, '合伙人要退股怎么谈条件?',
{ title: '融资合伙', desc: '融资、合伙、股权', samples: ['现在适合融资吗?', '合伙人要退股怎么谈条件?', '出让多少股权合适?', '银行贷款还是股权融资?'] }, '哪些业务可以外包?',
{ title: '转型外包', desc: '转型、外包、调整', samples: ['业务转型方向怎么选?', '哪些业务可以外包?', '转型期间现金流怎么管理?', '外包质量怎么控?'] },
] ]
export function Decisions() { export function Decisions() {
const [topics, setTopics] = useState<Topic[]>(FALLBACK_TOPICS)
const [selectedTopic, setSelectedTopic] = useState(0)
const [topicsCollapsed, setTopicsCollapsed] = useState(false)
const [loadingTopics, setLoadingTopics] = useState(false)
/** 换一批:调用 AI 动态生成话题(避免重复) */
const refreshTopics = useCallback(async () => {
setLoadingTopics(true)
try {
const res = await api.getTopics('decision', topics.map((t) => t.title))
if (res.topics?.length > 0) {
setTopics(res.topics)
setSelectedTopic(0)
}
} catch {
// AI 不可用时保持当前话题
} finally {
setLoadingTopics(false)
}
}, [topics])
const currentTopic = topics[selectedTopic]
return ( return (
<PageLayout <PageLayout
title="经营决策助手" title="经营决策"
description="项目承接、扩张收缩、招人、降价、业务线调整、融资合伙。情景推演,把利弊讲透。" description="扩张收缩、项目承接、定价策略、招人裁员、融资合伙、转型外包。AI 辅助分析,最终决策由你做主。"
> >
<BrainTrustSwitcher /> <BrainTrustSwitcher />
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<button
onClick={() => setTopicsCollapsed(!topicsCollapsed)}
className="flex items-center gap-1.5 text-base font-semibold transition-opacity hover:opacity-70"
>
<ChevronDown className={cn('h-4 w-4 transition-transform', topicsCollapsed && '-rotate-90')} />
决策话题
</button>
<button
onClick={refreshTopics}
disabled={loadingTopics}
className="flex items-center gap-1 text-xs text-muted-foreground transition-opacity hover:opacity-70 disabled:opacity-50"
>
{loadingTopics ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />}
换一批
</button>
</div>
</CardHeader>
{!topicsCollapsed && (
<CardContent>
{loadingTopics && (
<div className="flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
AI 正在生成新话题…
</div>
)}
<div className={cn('grid gap-3 sm:grid-cols-2 lg:grid-cols-3', loadingTopics && 'pointer-events-none opacity-50')}>
{topics.map((t, i) => (
<button
key={t.title + i}
onClick={() => setSelectedTopic(i)}
className={cn(
'flex items-start gap-3 rounded-md border p-3 text-left transition-colors',
selectedTopic === i
? 'border-primary bg-primary/5 ring-1 ring-primary'
: 'border-input hover:bg-accent',
)}
>
<TrendingUp className="mt-0.5 h-5 w-5 shrink-0 text-primary" aria-hidden />
<div>
<p className="text-sm font-medium">{t.title}</p>
<p className="mt-0.5 text-xs text-muted-foreground">{t.desc}</p>
</div>
</button>
))}
</div>
</CardContent>
)}
</Card>
{currentTopic && (
<ChatPanel <ChatPanel
scene="decision" scene="decision"
samples={currentTopic.samples} samples={FALLBACK_SAMPLES}
topic={currentTopic.title} topic="经营决策"
defaultMode="normal" defaultMode="normal"
placeholder={`描述你的${currentTopic.title}问题…`} placeholder="描述你的经营决策问题…"
showRiskCard={false} footer="AI 输出仅供参考,重大决策请结合实际情况和专业人士意见。"
enableUpload={false}
footer="AI 提供情景推演和利弊分析,最终决策由老板做出。"
/> />
)}
</PageLayout> </PageLayout>
) )
} }
+18 -102
View File
@@ -1,58 +1,28 @@
/** 私人法务页:话题选择 + 换一批 + ChatPanel /** 私人法务页:直接展示一批问题,点击即问
* scene='legal', 默认保密模式, 显示风险卡, 启用上传 * scene='legal', 默认保密模式, 显示风险卡, 启用上传
* 顶部含智囊场景切换栏(法务/决策/生活/情绪) * 顶部含智囊场景切换栏(法务/决策/生活/情绪)
*/ */
import { useState, useCallback } from 'react'
import { PageLayout } from '@/components/common/PageLayout' import { PageLayout } from '@/components/common/PageLayout'
import { ChatPanel } from '@/components/common/ChatPanel' import { ChatPanel } from '@/components/common/ChatPanel'
import { BrainTrustSwitcher } from '@/components/common/BrainTrustSwitcher' 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'
import { api } from '@/lib/api'
/** 话题类型 */ /** 初始兜底示例问题(覆盖劳动/合同/债务/股权/税务/诉讼高频场景) */
interface Topic { const FALLBACK_SAMPLES: string[] = [
title: string '员工被辞退要赔多少?',
desc: string '合同违约金怎么约定?',
samples: string[] '客户拖欠货款3个月如何催收?',
} '合伙人要退股怎么谈?',
'收到税务稽查通知怎么办?',
/** 私人法务:初始兜底话题,换一批时 AI 动态生成 */ '打官司要准备哪些证据?',
// 初始兜底话题(同步显示,无 loading) '员工工伤后能开除吗?',
const FALLBACK_TOPICS: Topic[] = [ '对方不履行合同怎么办?',
{ title: '劳动用工', desc: '工伤、辞退、欠薪、社保', samples: ['员工工伤后能开除吗?', '员工被辞退要赔多少?', '员工不签合同有何风险?', '社保未缴全被查怎么办?'] }, '借条怎么写才有效?',
{ title: '合同审查', desc: '违约、条款、效力', samples: ['合同违约金怎么约定?', '对方不履行合同怎么办?', '口头协议有法律效力吗?', '合同条款不公平能改吗?'] }, '股东不出资怎么办?',
{ title: '债权债务', desc: '欠款、催收、诉讼', samples: ['客户拖欠货款3个月如何催收?', '借条怎么写才有效?', '债务人不还钱能报警吗?', '超过诉讼时效怎么办?'] }, '发票开错了怎么处理?',
{ title: '股东纠纷', desc: '股权、分红、退股', samples: ['合伙人要退股怎么谈?', '股东不出资怎么办?', '分红比例怎么定?', '小股东权益被侵害怎么办?'] }, '赢了官司对方不执行怎么办?',
{ title: '税务风险', desc: '稽查、发票、合规', samples: ['收到税务稽查通知怎么办?', '发票开错了怎么处理?', '个体户怎么交税最合理?', '税务罚款能减免吗?'] },
{ title: '诉讼仲裁', desc: '起诉、举证、执行', samples: ['打官司要准备哪些证据?', '仲裁和诉讼哪个更快?', '赢了官司对方不执行怎么办?', '诉讼费用怎么算?'] },
] ]
export function LegalAdvisor() { export function LegalAdvisor() {
const [topics, setTopics] = useState<Topic[]>(FALLBACK_TOPICS)
const [selectedTopic, setSelectedTopic] = useState(0)
const [topicsCollapsed, setTopicsCollapsed] = useState(false)
const [loadingTopics, setLoadingTopics] = useState(false)
/** 换一批:调用 AI 动态生成话题(避免重复) */
const refreshTopics = useCallback(async () => {
setLoadingTopics(true)
try {
const res = await api.getTopics('legal', topics.map((t) => t.title))
if (res.topics?.length > 0) {
setTopics(res.topics)
setSelectedTopic(0)
}
} catch {
// AI 不可用时保持当前话题
} finally {
setLoadingTopics(false)
}
}, [topics])
const currentTopic = topics[selectedTopic]
return ( return (
<PageLayout <PageLayout
title="私人法务" title="私人法务"
@@ -60,70 +30,16 @@ export function LegalAdvisor() {
> >
<BrainTrustSwitcher /> <BrainTrustSwitcher />
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<button
onClick={() => setTopicsCollapsed(!topicsCollapsed)}
className="flex items-center gap-1.5 text-base font-semibold transition-opacity hover:opacity-70"
>
<ChevronDown className={cn('h-4 w-4 transition-transform', topicsCollapsed && '-rotate-90')} />
法务话题
</button>
<button
onClick={refreshTopics}
disabled={loadingTopics}
className="flex items-center gap-1 text-xs text-muted-foreground transition-opacity hover:opacity-70 disabled:opacity-50"
>
{loadingTopics ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />}
换一批
</button>
</div>
</CardHeader>
{!topicsCollapsed && (
<CardContent>
{loadingTopics && (
<div className="flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
AI 正在生成新话题…
</div>
)}
<div className={cn('grid gap-3 sm:grid-cols-2 lg:grid-cols-3', loadingTopics && 'pointer-events-none opacity-50')}>
{topics.map((t, i) => (
<button
key={t.title + i}
onClick={() => setSelectedTopic(i)}
className={cn(
'flex items-start gap-3 rounded-md border p-3 text-left transition-colors',
selectedTopic === i
? 'border-primary bg-primary/5 ring-1 ring-primary'
: 'border-input hover:bg-accent',
)}
>
<Briefcase className="mt-0.5 h-5 w-5 shrink-0 text-primary" aria-hidden />
<div>
<p className="text-sm font-medium">{t.title}</p>
<p className="mt-0.5 text-xs text-muted-foreground">{t.desc}</p>
</div>
</button>
))}
</div>
</CardContent>
)}
</Card>
{currentTopic && (
<ChatPanel <ChatPanel
scene="legal" scene="legal"
samples={currentTopic.samples} samples={FALLBACK_SAMPLES}
topic={currentTopic.title} topic="法务咨询"
defaultMode="confidential" defaultMode="confidential"
placeholder={`描述你的${currentTopic.title}问题…`} placeholder="描述你的法务问题…"
showRiskCard showRiskCard
enableUpload enableUpload
footer="AI 输出仅供参考,不构成律师、会计师、税务师或医生的正式意见。重大风险事项请寻求执业专业人士复核。" footer="AI 输出仅供参考,不构成律师、会计师、税务师或医生的正式意见。重大风险事项请寻求执业专业人士复核。"
/> />
)}
</PageLayout> </PageLayout>
) )
} }
+19 -105
View File
@@ -1,127 +1,41 @@
/** 生活助理页:话题选择 + 换一批 + ChatPanel /** 生活助理页:直接展示一批问题,点击即问
* scene='life', 默认保密模式 * scene='life', 默认保密模式
*/ */
import { useState, useCallback } from 'react'
import { PageLayout } from '@/components/common/PageLayout' import { PageLayout } from '@/components/common/PageLayout'
import { ChatPanel } from '@/components/common/ChatPanel' import { ChatPanel } from '@/components/common/ChatPanel'
import { BrainTrustSwitcher } from '@/components/common/BrainTrustSwitcher' 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'
import { api } from '@/lib/api'
/** 话题类型 */ /** 初始兜底示例问题(覆盖家庭/教育/养老/睡眠/出差/健康高频场景) */
interface Topic { const FALLBACK_SAMPLES: string[] = [
title: string '怎么和青春期的孩子沟通?',
desc: string '孩子不想上学怎么办?',
samples: string[] '父母年纪大了养老方案怎么选?',
} '最近睡眠很差怎么改善?',
'出差频繁怎么兼顾家庭?',
/** 生活助理:初始兜底话题,换一批时 AI 动态生成 */ '体检报告几项异常怎么办?',
const FALLBACK_TOPICS: Topic[] = [ '夫妻因工作吵架怎么办?',
{ title: '家庭沟通', desc: '亲子、夫妻、关系', samples: ['怎么和青春期的孩子沟通?', '夫妻因工作吵架怎么办?', '家人不支持创业怎么说服?', '工作太忙忽略家人怎么办?'] }, '兴趣班该报几个?',
{ title: '子女教育', desc: '升学、兴趣、规划', samples: ['孩子不想上学怎么办?', '兴趣班该报几个?', '要不要送孩子出国?', '孩子成绩下滑怎么沟通?'] }, '异地父母怎么照顾?',
{ title: '父母养老', desc: '养老、医疗、陪伴', samples: ['父母年纪大了养老方案怎么选?', '异地父母怎么照顾?', '养老院还是居家养老?', '父母医疗费用怎么分担?'] }, '工作压力大怎么缓解?',
{ title: '睡眠与压力', desc: '失眠、焦虑、调节', samples: ['最近睡眠很差怎么改善?', '工作压力大怎么缓解?', '焦虑到影响工作怎么办?', '怎么判断需要看心理医生?'] }, '行程太满怎么优化?',
{ title: '出差日程', desc: '出行、时间管理', samples: ['出差频繁怎么兼顾家庭?', '行程太满怎么优化?', '出差期间公司怎么管?', '如何减少无效出差?'] }, '久坐怎么改善健康?',
{ title: '健康习惯', desc: '体检、饮食、运动', samples: ['体检报告几项异常怎么办?', '久坐怎么改善健康?', '应酬多怎么控制饮食?', '没时间运动怎么办?'] },
] ]
export function LifeAssistant() { export function LifeAssistant() {
const [topics, setTopics] = useState<Topic[]>(FALLBACK_TOPICS)
const [selectedTopic, setSelectedTopic] = useState(0)
const [topicsCollapsed, setTopicsCollapsed] = useState(false)
const [loadingTopics, setLoadingTopics] = useState(false)
/** 换一批:调用 AI 动态生成话题(避免重复) */
const refreshTopics = useCallback(async () => {
setLoadingTopics(true)
try {
const res = await api.getTopics('life', topics.map((t) => t.title))
if (res.topics?.length > 0) {
setTopics(res.topics)
setSelectedTopic(0)
}
} catch {
// AI 不可用时保持当前话题
} finally {
setLoadingTopics(false)
}
}, [topics])
const currentTopic = topics[selectedTopic]
return ( return (
<PageLayout <PageLayout
title="生活助理" title="生活助理"
description="家庭沟通、子女教育、父母养老、健康习惯、出行规划。老板的生活问题,也值得认真对待。" description="家庭沟通、子女教育、父母养老、睡眠与压力、出差日程、健康习惯。"
> >
<BrainTrustSwitcher /> <BrainTrustSwitcher />
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<button
onClick={() => setTopicsCollapsed(!topicsCollapsed)}
className="flex items-center gap-1.5 text-base font-semibold transition-opacity hover:opacity-70"
>
<ChevronDown className={cn('h-4 w-4 transition-transform', topicsCollapsed && '-rotate-90')} />
生活话题
</button>
<button
onClick={refreshTopics}
disabled={loadingTopics}
className="flex items-center gap-1 text-xs text-muted-foreground transition-opacity hover:opacity-70 disabled:opacity-50"
>
{loadingTopics ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />}
换一批
</button>
</div>
</CardHeader>
{!topicsCollapsed && (
<CardContent>
{loadingTopics && (
<div className="flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
AI 正在生成新话题…
</div>
)}
<div className={cn('grid gap-3 sm:grid-cols-2 lg:grid-cols-3', loadingTopics && 'pointer-events-none opacity-50')}>
{topics.map((t, i) => (
<button
key={t.title + i}
onClick={() => setSelectedTopic(i)}
className={cn(
'flex items-start gap-3 rounded-md border p-3 text-left transition-colors',
selectedTopic === i
? 'border-primary bg-primary/5 ring-1 ring-primary'
: 'border-input hover:bg-accent',
)}
>
<Heart className="mt-0.5 h-5 w-5 shrink-0 text-primary" aria-hidden />
<div>
<p className="text-sm font-medium">{t.title}</p>
<p className="mt-0.5 text-xs text-muted-foreground">{t.desc}</p>
</div>
</button>
))}
</div>
</CardContent>
)}
</Card>
{currentTopic && (
<ChatPanel <ChatPanel
scene="life" scene="life"
samples={currentTopic.samples} samples={FALLBACK_SAMPLES}
topic={currentTopic.title} topic="生活助理"
defaultMode="confidential" defaultMode="confidential"
placeholder={`描述你的${currentTopic.title}问题…`} placeholder="描述你的生活问题…"
showRiskCard={false}
enableUpload={false}
footer="AI 输出仅供参考,重大事项请咨询相关领域专业人士。"
/> />
)}
</PageLayout> </PageLayout>
) )
} }