Files
BossAi/web2/src/pages/MyProfile.tsx
T
selfrelease 02e1babdc4
CI / 后端测试 (push) Has been cancelled
CI / 前端构建 (push) Has been cancelled
fix: 个案事实key_facts补充200+中文映射,删除重复jurisdiction
2026-09-01 10:20:50 +08:00

1105 lines
41 KiB
TypeScript

import { useEffect, useState, useCallback } from 'react'
import { UserCircle, History, ChevronRight, ArrowLeft, ChevronDown, Pencil, Plus, Trash2 } from 'lucide-react'
import { PageLayout } from '@/components/common/PageLayout'
import { EmptyState } from '@/components/common/EmptyState'
import { LoadingState } from '@/components/common/LoadingState'
import { ErrorState } from '@/components/common/ErrorState'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Input } from '@/components/ui/input'
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
} from '@/components/ui/dialog'
import { api } from '@/lib/api'
import { cn, formatTime } from '@/lib/utils'
/** 用户画像数据 */
interface ProfileData {
phone: string
industry: string
company_size: string
role: string
common_scenes: string[]
common_topics: string[]
risk_tolerance: string
communication_style: string
decision_style: string
key_facts: Record<string, string>
interaction_count: number
last_active: string
}
/** 画像历史版本 */
interface ProfileHistory {
id: number
version: number
profile: Record<string, unknown>
source: string
created_at: string
}
/** 字段中文标签 */
const FIELD_LABELS: Record<string, string> = {
industry: '行业',
company_size: '企业规模',
role: '角色',
risk_tolerance: '风险偏好',
communication_style: '沟通偏好',
decision_style: '决策风格',
}
/** 企业规模映射 */
const SIZE_LABELS: Record<string, string> = {
micro: '微型(<10人)',
small: '小型(10-50人)',
medium: '中型(50-200人)',
large: '大型(200+人)',
}
/** 风险偏好映射 */
const RISK_LABELS: Record<string, string> = {
conservative: '保守型',
moderate: '稳健型',
aggressive: '进取型',
}
/** 角色映射 */
const ROLE_LABELS: Record<string, string> = {
owner: '老板/创始人',
manager: '管理者',
ceo: 'CEO',
director: '总监',
}
/** 决策风格映射 */
const DECISION_STYLE_LABELS: Record<string, string> = {
'data-driven': '数据驱动型',
intuitive: '直觉型',
collaborative: '协作型',
}
/** 沟通偏好映射 */
const COMMUNICATION_STYLE_LABELS: Record<string, string> = {
concise: '简洁',
detailed: '详细',
structured: '结构化',
}
/** 行业映射 */
const INDUSTRY_LABELS: Record<string, string> = {
retail: '零售',
manufacturing: '制造业',
tech: '科技',
service: '服务业',
finance: '金融',
realestate: '房地产',
education: '教育',
healthcare: '医疗健康',
construction: '建筑',
logistics: '物流',
agriculture: '农业',
hospitality: '餐饮酒店',
}
/** key_facts 的 key 中文映射 */
const KEY_FACT_LABELS: Record<string, string> = {
region: '地区',
company_type: '公司类型',
team_ability: '团队能力',
communication: '沟通情况',
medical_proof: '医疗证明',
contract_terms: '合同条款',
medical_period: '医疗期',
financial_status: '财务状况',
injury_situation: '工伤情况',
legal_procedures: '法律程序',
market_potential: '市场潜力',
alternative_option: '备选方案',
work_injury_details: '工伤详情',
alternative_solution: '替代方案',
work_injury_insurance: '工伤保险',
performance_assessment: '绩效评估',
relevant_legal_provisions: '相关法律条款',
employee_count: '员工人数',
revenue: '营收',
profit_status: '盈利状况',
business_model: '商业模式',
funding_stage: '融资阶段',
expansion_plan: '扩张计划',
competitor: '竞争对手',
customer_concentration: '客户集中度',
supply_chain: '供应链',
cash_flow: '现金流',
asset_status: '资产状况',
liability: '负债',
contract_dispute: '合同纠纷',
debt_amount: '债务金额',
tax_risk: '税务风险',
litigation_status: '诉讼状态',
shareholder_structure: '股权结构',
partnership: '合伙关系',
salary_status: '薪资状况',
social_security: '社保',
severance: '经济补偿',
working_years: '工作年限',
probation_period: '试用期',
termination_reason: '解雇原因',
overtime_status: '加班情况',
export_capability: '出口能力',
family_situation: '家庭情况',
health_status: '健康状况',
travel_plan: '出行计划',
important_event: '重要事项',
education_background: '教育背景',
stress_source: '压力来源',
personality: '性格特征',
work_time: '工作时间',
contract_status: '合同状态',
salary_calculation_basis: '薪资计算依据',
injury_detail: '受伤详情',
treatment_status: '治疗状况',
recovery_status: '康复状况',
compensation_claim: '赔偿诉求',
arbitration_status: '仲裁状态',
employment_relationship: '劳动关系',
wage_standard: '工资标准',
working_hours: '工时',
leave_record: '请假记录',
discipline_record: '违纪记录',
training_agreement: '培训协议',
confidentiality_agreement: '保密协议',
non_compete: '竞业限制',
ip_ownership: '知识产权归属',
data_protection: '数据保护',
compliance_status: '合规状况',
regulatory_requirement: '监管要求',
license_status: '许可状态',
audit_finding: '审计发现',
internal_control: '内部控制',
risk_management: '风险管理',
insurance_coverage: '保险覆盖',
claim_status: '理赔状态',
policy_detail: '保单详情',
premium_status: '保费状态',
beneficiary: '受益人',
coverage_amount: '保额',
deductible: '免赔额',
policy_period: '保险期间',
renewal_status: '续保状态',
cancellation_reason: '退保原因',
loan_purpose: '借款用途',
repayment_plan: '还款计划',
collateral: '抵押物',
guarantor_info: '担保人',
interest_rate: '利率',
penalty_rate: '罚息率',
overdue_days: '逾期天数',
collection_status: '催收状态',
settlement_status: '结算状态',
bankruptcy_status: '破产状态',
restructure_plan: '重组方案',
liquidation_status: '清算状态',
debt_restructuring: '债务重组',
asset_disposal: '资产处置',
business_transfer: '业务转让',
merger_status: '并购状态',
acquisition_status: '收购状态',
divestiture: '剥离',
joint_venture: '合资',
strategic_alliance: '战略联盟',
ip_valuation: '知识产权估值',
technology_transfer: '技术转让',
royalty_rate: '许可费率',
infringement_scope: '侵权范围',
damages_claim: '损害赔偿诉求',
injunction_status: '禁令状态',
invalidation_request: '无效宣告请求',
opposition_status: '异议状态',
licensing_term: '许可期限',
territory: '地域范围',
exclusivity: '排他性',
sublicensing: '再许可',
warranty_term: '保证条款',
indemnity: '补偿条款',
limitation_liability: '责任限制',
force_majeure: '不可抗力',
dispute_resolution: '争议解决',
governing_law: '适用法律',
jurisdiction: '管辖',
arbitration_clause: '仲裁条款',
mediation_status: '调解状态',
enforcement_status: '执行状态',
execution_status: '执行状况',
preservation_measure: '保全措施',
objection_status: '异议状态',
retrial_request: '再审请求',
appeal_status: '上诉状态',
court_level: '法院层级',
case_procedure: '案件程序',
hearing_date: '开庭日期',
evidence_status: '举证状况',
witness_status: '证人状况',
expert_evaluation: '专家鉴定',
judicial_interpretation: '司法解释',
precedent_case: '先例案例',
legislation_reference: '法律依据',
// 补充:AI 分析维度生成的 key
evidence: '证据',
amount: '涉及金额',
party: '对方当事人',
timeline: '时间线',
contract: '合同',
scale: '决策规模',
cashflow: '现金流',
alternatives: '备选方案',
market: '市场状况',
team: '团队',
risk_tolerance: '风险承受',
profitability: '盈利能力',
situation: '情境',
people: '涉及人员',
duration: '持续时间',
attempt: '已尝试方式',
goal: '期望结果',
feeling: '情绪状态',
trigger: '触发事件',
time_range: '时间范围',
region_policy: '地区政策',
employer_status: '雇主身份',
has_written_contract_agreement: '是否有书面合同',
event_occurrence_and_current_status: '事件发生及现状',
notification_negotiation: '通知与协商',
labor_contract: '劳动合同',
performance_evaluation: '绩效评估',
salary_basis: '薪资计算依据',
legal_provisions: '法律条款',
alternative_plan: '备选方案',
replacement_plan: '替代方案',
financial_situation: '财务状况',
// 个案事实 key 补充
contract_exists: '是否有合同',
evidence_available: '是否有证据',
internal_procedure_exists: '是否有内部程序',
has_contract: '是否有合同',
has_evidence: '是否有证据',
has_internal_procedure: '是否有内部程序',
written_contract: '书面合同',
witness: '证人',
document_evidence: '书面证据',
electronic_evidence: '电子证据',
physical_evidence: '物证',
damage_amount: '损失金额',
dispute_amount: '争议金额',
claim_amount: '索赔金额',
response_deadline: '答复期限',
statute_limitations: '诉讼时效',
applicable_law: '适用法律',
cause_of_action: '案由',
filing_status: '立案状态',
court: '法院',
judge: '法官',
lawyer: '律师',
opposing_party: '对方当事人',
third_party: '第三方',
mediation_attempted: '是否尝试调解',
arbitration_attempted: '是否尝试仲裁',
litigation_attempted: '是否尝试诉讼',
settlement_attempted: '是否尝试和解',
negotiation_status: '协商状态',
breach_type: '违约类型',
breach_severity: '违约程度',
damage_type: '损失类型',
compensation_type: '赔偿类型',
compensation_amount: '赔偿金额',
liability_type: '责任类型',
liability_amount: '责任金额',
insurance_type: '保险类型',
policy_number: '保单号',
claim_amount_insurance: '保险理赔金额',
deductible_amount: '免赔额',
coverage_type: '保障类型',
renewal_date: '续保日期',
beneficiary_name: '受益人',
loan_amount: '贷款金额',
loan_type: '贷款类型',
lender: '出借方',
borrower: '借款方',
guarantor: '担保人',
collateral_type: '抵押物类型',
collateral_value: '抵押物价值',
interest_rate_type: '利率类型',
repayment_method: '还款方式',
monthly_payment: '月供',
overdue_amount: '逾期金额',
overdue_days_count: '逾期天数',
collection_method: '催收方式',
default_risk: '违约风险',
early_repayment: '提前还款',
grace_period: '宽限期',
penalty_interest: '罚息',
liquidated_damages: '违约金',
termination_clause: '终止条款',
renewal_clause: '续约条款',
exclusivity_clause: '排他条款',
non_compete_clause: '竞业条款',
confidentiality_clause: '保密条款',
ip_clause: '知识产权条款',
dispute_resolution_clause: '争议解决条款',
governing_law_clause: '适用法律条款',
force_majeure_clause: '不可抗力条款',
assignment_clause: '转让条款',
sublease_clause: '转租条款',
service_scope: '服务范围',
service_period: '服务期限',
service_fee: '服务费',
payment_terms: '付款条件',
payment_method_contract: '合同付款方式',
delivery_terms: '交付条件',
delivery_method: '交付方式',
delivery_address: '交付地址',
delivery_date: '交付日期',
acceptance_criteria: '验收标准',
acceptance_period: '验收期限',
warranty_period: '质保期',
warranty_scope: '质保范围',
maintenance_service: '维护服务',
technical_support: '技术支持',
training_service: '培训服务',
consulting_service: '咨询服务',
outsourcing_service: '外包服务',
leasing_service: '租赁服务',
licensing_service: '许可服务',
distribution_service: '分销服务',
agency_service: '代理服务',
brokerage_service: '经纪服务',
freight_service: '货运服务',
warehousing_service: '仓储服务',
customs_service: '报关服务',
inspection_service: '检验服务',
certification_service: '认证服务',
notarization_service: '公证服务',
legal_opinion: '法律意见',
legal_review: '法律审查',
due_diligence: '尽职调查',
compliance_review: '合规审查',
risk_assessment: '风险评估',
feasibility_study: '可行性研究',
market_research: '市场调研',
customer_survey: '客户调研',
competitor_analysis: '竞品分析',
swot_analysis: 'SWOT分析',
financial_analysis: '财务分析',
cost_analysis: '成本分析',
profit_analysis: '利润分析',
cashflow_analysis: '现金流分析',
roi_analysis: '投资回报分析',
break_even_analysis: '盈亏平衡分析',
sensitivity_analysis: '敏感性分析',
scenario_analysis: '情景分析',
valuation: '估值',
valuation_method: '估值方法',
discount_rate: '折现率',
terminal_value: '终值',
enterprise_value: '企业价值',
equity_value: '股权价值',
per_share_value: '每股价值',
goodwill: '商誉',
intangible_assets: '无形资产',
tangible_assets: '有形资产',
fixed_assets: '固定资产',
current_assets: '流动资产',
inventory: '存货',
accounts_receivable: '应收账款',
accounts_payable: '应付账款',
prepaid_expenses: '预付费用',
accrued_expenses: '应计费用',
deferred_revenue: '递延收入',
deferred_tax: '递延税',
tax_rate: '税率',
tax_type: '税种',
tax_period: '税款所属期',
tax_filing: '纳税申报',
tax_payment: '税款缴纳',
tax_refund: '退税',
tax_exemption: '免税',
tax_deduction: '税前扣除',
tax_credit: '税收抵免',
transfer_pricing: '转让定价',
customs_duty: '关税',
vat: '增值税',
corporate_tax: '企业所得税',
individual_tax: '个人所得税',
property_tax: '房产税',
land_tax: '土地税',
stamp_duty: '印花税',
vehicle_tax: '车船税',
environmental_tax: '环保税',
other_tax: '其他税',
// 通用兜底:AI 可能生成的其他 key
has_written_contract: '是否有书面合同',
has_oral_agreement: '是否有口头协议',
has_email_record: '是否有邮件记录',
has_chat_record: '是否有聊天记录',
has_signed_agreement: '是否有签署协议',
has_notarized_document: '是否有公证文件',
has_registered_trademark: '是否有注册商标',
has_patent: '是否有专利',
has_copyright: '是否有版权',
has_trade_secret: '是否有商业秘密',
has_non_compete: '是否有竞业限制',
has_confidentiality: '是否有保密协议',
has_employment_contract: '是否有劳动合同',
has_service_agreement: '是否有服务协议',
has_leasing_agreement: '是否有租赁协议',
has_purchase_order: '是否有采购订单',
has_sales_contract: '是否有销售合同',
has_distribution_agreement: '是否有分销协议',
has_agency_agreement: '是否有代理协议',
has_partnership_agreement: '是否有合伙协议',
has_shareholders_agreement: '是否有股东协议',
has_board_resolution: '是否有董事会决议',
has_shareholder_resolution: '是否有股东会决议',
has_business_license: '是否有营业执照',
has_special_permit: '是否有特许经营许可',
has_industry_qualification: '是否有行业资质',
has_environmental_approval: '是否有环评审批',
has_safety_approval: '是否有安全审批',
has_fire_approval: '是否有消防审批',
has_food_license: '是否有食品经营许可',
has_medical_license: '是否有医疗器械许可',
has_cosmetic_license: '是否有化妆品许可',
has_import_export_license: '是否有进出口许可',
has_customs_registration: '是否有海关登记',
has_foreign_exchange_registration: '是否有外汇登记',
has_tax_registration: '是否有税务登记',
has_social_security_registration: '是否有社保登记',
has_housing_fund_registration: '是否有公积金登记',
}
/** key_facts 的值中文映射 */
const KEY_FACT_VALUE_LABELS: Record<string, string> = {
yes: '是',
no: '否',
true: '是',
false: '否',
none: '无',
unknown: '未知',
'n/a': '不适用',
pending: '待定',
active: '活跃',
inactive: '停用',
profit: '盈利',
loss: '亏损',
expansion: '扩张',
contraction: '收缩',
stable: '稳定',
growth: '增长中',
declining: '下滑中',
...SIZE_LABELS,
...RISK_LABELS,
...ROLE_LABELS,
...INDUSTRY_LABELS,
// 补充值翻译
employer: '雇主',
employee: '员工',
'contract review': '合同审查',
'work-life balance': '工作生活平衡',
'has growth potential': '有增长潜力',
'separate financing': '单独融资',
'change product line': '改变产品线',
'compulsory payment': '强制缴纳',
'can terminate if unqualified': '不合格可以辞退',
'can dismiss if unqualified': '不合格可以解除合同',
'in negotiation': '在协商',
'loss 1 million': '亏损100万',
'has written contract': '有书面合同',
'no written contract': '无书面合同',
'limited company': '有限公司',
'not capable': '不具备',
'can terminate': '可以解除',
'yes, recognized': '是,已认定',
'incompetent': '不胜任',
'communicated': '沟通了',
'not communicated': '未沟通',
'clear standards': '有明确评估标准和记录',
'6 months ago': '6个月前',
'last year': '去年',
'has ended': '已结束',
'yes, mandatory': '是,强制',
}
/** 场景中文映射 */
const SCENE_LABELS: Record<string, string> = {
legal: '私人法务',
decision: '经营决策',
life: '生活助理',
emotion: '情绪中心',
}
/** 来源映射 */
const SOURCE_LABELS: Record<string, string> = {
auto: 'AI 自动提取',
manual: '用户确认',
}
/** 画像特征 key(稳定的用户属性,直接展示) */
const PROFILE_FACT_KEYS = new Set([
'region', 'company_type', 'team_ability', 'industry', 'company_size',
'role', 'risk_tolerance', 'communication_style', 'decision_style',
'market_potential', 'financial_status', 'financial_situation',
'cash_flow', 'asset_status', 'liability', 'revenue', 'profit_status',
'business_model', 'funding_stage', 'expansion_plan', 'competitor',
'customer_concentration', 'supply_chain', 'employee_count',
'shareholder_structure', 'partnership',
])
/** 判断 key 属于画像特征还是个案事实 */
function isProfileFact(key: string): boolean {
return PROFILE_FACT_KEYS.has(String(key).toLowerCase())
}
const TOPIC_LABELS: Record<string, string> = {
'contract review': '合同审查',
'work-life balance': '工作生活平衡',
'tax': '税务',
'expansion': '扩张',
'financing': '融资',
'labor': '劳动用工',
'employment': '劳动用工',
'litigation': '诉讼',
'contract': '合同',
'shareholder': '股东',
'debt': '债务',
'salary': '薪资',
'social security': '社保',
'work injury': '工伤',
'termination': '辞退',
'decision': '决策',
'pricing': '定价',
'hiring': '招聘',
'layoff': '裁员',
'partnership': '合伙',
'investment': '投资',
'family': '家庭',
'health': '健康',
'stress': '压力',
'sleep': '睡眠',
'parenting': '亲子',
'education': '教育',
'marriage': '婚姻',
}
/** 翻译常问话题 */
function translateTopic(topic: string): string {
const t = String(topic)
const lower = t.toLowerCase().trim()
return TOPIC_LABELS[lower] || t
}
/** 格式化字段值 */
function formatValue(key: string, value: string): string {
const v = String(value)
if (key === 'company_size') return SIZE_LABELS[v] || v
if (key === 'risk_tolerance') return RISK_LABELS[v] || v
if (key === 'role') return ROLE_LABELS[v] || v
if (key === 'decision_style') return DECISION_STYLE_LABELS[v] || v
if (key === 'communication_style') return COMMUNICATION_STYLE_LABELS[v] || v
if (key === 'industry') return INDUSTRY_LABELS[v] || v
return v
}
/** 翻译 key_facts 的 key */
function translateFactKey(key: string): string {
const k = String(key).toLowerCase()
return KEY_FACT_LABELS[k] || String(key)
}
/** 翻译 key_facts 的值 */
function translateFactValue(value: string): string {
const v = String(value)
const lower = v.toLowerCase().trim()
return KEY_FACT_VALUE_LABELS[lower] || v
}
export function MyProfile() {
const [profile, setProfile] = useState<ProfileData | null>(null)
const [history, setHistory] = useState<ProfileHistory[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [currentHistory, setCurrentHistory] = useState<ProfileHistory | null>(null)
const [showCaseFacts, setShowCaseFacts] = useState(false)
const [editing, setEditing] = useState<{ field: string; label: string; value: string; type: 'select' | 'text' } | null>(null)
const [editValue, setEditValue] = useState('')
const [saving, setSaving] = useState(false)
const [addingFact, setAddingFact] = useState(false)
const [newFactKey, setNewFactKey] = useState('')
const [newFactValue, setNewFactValue] = useState('')
const [deletingFact, setDeletingFact] = useState<{ key: string; label: string } | null>(null)
const load = useCallback(async () => {
setLoading(true)
setError(null)
try {
const [profileRes, historyRes] = await Promise.all([
api.getProfile(),
api.getProfileHistory(),
])
setProfile(profileRes as unknown as ProfileData)
setHistory(historyRes.results)
} catch (e) {
setError(e instanceof Error ? e.message : '加载失败')
} finally {
setLoading(false)
}
}, [])
useEffect(() => { load() }, [load])
/** 标准字段选项(下拉选择) */
const FIELD_OPTIONS: Record<string, { value: string; label: string }[]> = {
industry: Object.entries(INDUSTRY_LABELS).map(([v, l]) => ({ value: v, label: l })),
company_size: Object.entries(SIZE_LABELS).map(([v, l]) => ({ value: v, label: l })),
role: Object.entries(ROLE_LABELS).map(([v, l]) => ({ value: v, label: l })),
risk_tolerance: Object.entries(RISK_LABELS).map(([v, l]) => ({ value: v, label: l })),
communication_style: Object.entries(COMMUNICATION_STYLE_LABELS).map(([v, l]) => ({ value: v, label: l })),
decision_style: Object.entries(DECISION_STYLE_LABELS).map(([v, l]) => ({ value: v, label: l })),
}
/** 打开编辑 */
const handleEdit = (field: string, label: string, value: string) => {
const isSelect = !!FIELD_OPTIONS[field]
setEditing({ field, label, value, type: isSelect ? 'select' : 'text' })
setEditValue(value)
}
/** 保存编辑 */
const handleSave = async () => {
if (!editing || !editValue.trim()) { setEditing(null); return }
setSaving(true)
try {
const field = editing.field
if (FIELD_LABELS[field]) {
// 标准字段
await api.updateProfile({ [field]: editValue })
setProfile((prev) => prev ? { ...prev, [field]: editValue } : prev)
} else {
// key_facts
await api.updateProfile({ key_facts: { [field]: editValue } })
setProfile((prev) => prev ? { ...prev, key_facts: { ...prev.key_facts, [field]: editValue } } : prev)
}
setEditing(null)
} catch (e) {
setError(e instanceof Error ? e.message : '保存失败')
} finally {
setSaving(false)
}
}
/** 删除画像特征(确认后执行) */
const handleDeleteFact = (key: string, label: string) => {
setDeletingFact({ key, label })
}
/** 确认删除 */
const confirmDeleteFact = async () => {
if (!deletingFact || !profile) return
try {
const newFacts = { ...profile.key_facts }
delete newFacts[deletingFact.key]
await api.updateProfile({ key_facts: newFacts })
setProfile((prev) => prev ? { ...prev, key_facts: newFacts } : prev)
setDeletingFact(null)
} catch (e) {
setError(e instanceof Error ? e.message : '删除失败')
}
}
/** 添加画像特征 */
const handleAddFact = async () => {
if (!newFactKey.trim() || !newFactValue.trim() || !profile) return
setSaving(true)
try {
const key = newFactKey.trim()
const newFacts = { ...profile.key_facts, [key]: newFactValue.trim() }
await api.updateProfile({ key_facts: newFacts })
setProfile((prev) => prev ? { ...prev, key_facts: newFacts } : prev)
setAddingFact(false)
setNewFactKey('')
setNewFactValue('')
} catch (e) {
setError(e instanceof Error ? e.message : '添加失败')
} finally {
setSaving(false)
}
}
if (loading) return <PageLayout title="我的画像"><LoadingState /></PageLayout>
if (error) return <PageLayout title="我的画像"><ErrorState message={error} onRetry={load} /></PageLayout>
if (!profile) return null
// ===== 历史详情视图 =====
if (currentHistory) {
const hp = currentHistory.profile as Record<string, unknown>
return (
<PageLayout
title={`画像版本 v${currentHistory.version}`}
description={`${SOURCE_LABELS[currentHistory.source] || currentHistory.source} · ${formatTime(currentHistory.created_at)}`}
actions={
<Button variant="outline" size="sm" onClick={() => setCurrentHistory(null)}>
<ArrowLeft className="h-3.5 w-3.5" /> 返回
</Button>
}
>
<Card>
<CardContent className="p-5">
<div className="space-y-3">
{/* 标准字段:多列网格 */}
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 sm:grid-cols-3">
{Object.entries(FIELD_LABELS).map(([key, label]) => {
const val = hp[key] as string
if (!val) return null
return (
<div key={key} className="flex items-center justify-between border-b pb-1">
<span className="text-xs text-muted-foreground">{label}</span>
<span className="text-xs font-medium">{formatValue(key, val)}</span>
</div>
)
})}
</div>
{Array.isArray(hp.common_topics) && (hp.common_topics as string[]).length > 0 && (
<div className="border-b pb-2">
<p className="mb-1 text-xs text-muted-foreground">常问话题</p>
<div className="flex flex-wrap gap-1">
{(hp.common_topics as string[]).map((t, i) => (
<Badge key={i} variant="secondary" className="text-[10px] px-1 py-0">{translateTopic(String(t))}</Badge>
))}
</div>
</div>
)}
{Boolean(hp.key_facts && typeof hp.key_facts === 'object' && !Array.isArray(hp.key_facts) && Object.keys(hp.key_facts as Record<string, unknown>).length > 0) && (
<div className="border-b pb-2">
<p className="mb-1 text-xs text-muted-foreground">关键事实</p>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-3">
{Object.entries(hp.key_facts as Record<string, string>).map(([k, v]) => (
<div key={k} className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">{translateFactKey(k)}</span>
<span className="font-medium">{translateFactValue(String(v))}</span>
</div>
))}
</div>
</div>
)}
{Array.isArray(hp.common_scenes) && (hp.common_scenes as string[]).length > 0 && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">常用场景</span>
<div className="flex flex-wrap gap-1">
{(hp.common_scenes as string[]).map((s, i) => (
<Badge key={i} variant="outline" className="text-[10px] px-1 py-0">
{SCENE_LABELS[String(s)] || String(s)}
</Badge>
))}
</div>
</div>
)}
</div>
</CardContent>
</Card>
</PageLayout>
)
}
// ===== 画像主视图 =====
return (
<PageLayout
title="我的画像"
description="系统根据你的对话累积的抽象特征,用于个性化回复。不存储对话原文。"
>
{/* 当前画像 */}
<Card>
<CardContent className="p-5">
<div className="mb-4 flex items-center gap-2">
<UserCircle className="h-5 w-5 text-primary" aria-hidden />
<h3 className="text-sm font-semibold">当前画像</h3>
<Badge variant="secondary" className="ml-auto text-xs">
交互 {profile.interaction_count} 次
</Badge>
</div>
<div className="space-y-3">
{/* 标准字段:多列网格(可编辑) */}
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 sm:grid-cols-3">
{Object.entries(FIELD_LABELS).map(([key, label]) => {
const val = profile[key as keyof ProfileData] as string
if (!val) return null
return (
<div key={key} className="group flex items-center justify-between border-b pb-1">
<span className="text-xs text-muted-foreground">{label}</span>
<div className="flex items-center gap-1">
<span className="text-xs font-medium">{formatValue(key, val)}</span>
<button
onClick={() => handleEdit(key, label, val)}
className="opacity-0 transition-opacity group-hover:opacity-100"
aria-label={`编辑${label}`}
>
<Pencil className="h-3 w-3 text-muted-foreground hover:text-primary" />
</button>
</div>
</div>
)
})}
</div>
{profile.common_topics && profile.common_topics.length > 0 && (
<div className="border-b pb-2">
<p className="mb-1 text-xs text-muted-foreground">常问话题</p>
<div className="flex flex-wrap gap-1">
{profile.common_topics.map((t, i) => (
<Badge key={i} variant="secondary" className="text-[10px] px-1 py-0">{translateTopic(t)}</Badge>
))}
</div>
</div>
)}
{profile.key_facts && Object.keys(profile.key_facts).length > 0 && (() => {
const entries = Object.entries(profile.key_facts)
const profileFacts = entries.filter(([k]) => isProfileFact(k))
const caseFacts = entries.filter(([k]) => !isProfileFact(k))
return (
<>
{/* 画像特征:直接展示(可编辑/可增删) */}
{profileFacts.length > 0 && (
<div className="border-b pb-2">
<div className="mb-1 flex items-center justify-between">
<p className="text-xs text-muted-foreground">画像特征</p>
<button
onClick={() => setAddingFact(true)}
className="flex items-center gap-0.5 text-xs text-primary hover:text-primary/80"
>
<Plus className="h-3 w-3" />添加
</button>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-3">
{profileFacts.map(([k, v]) => (
<div key={k} className="group flex items-center justify-between text-xs">
<span className="text-muted-foreground">{translateFactKey(k)}</span>
<div className="flex items-center gap-1">
<span className="font-medium">{translateFactValue(v)}</span>
<button
onClick={() => handleEdit(k, translateFactKey(k), v)}
className="opacity-0 transition-opacity group-hover:opacity-100"
aria-label={`编辑${translateFactKey(k)}`}
>
<Pencil className="h-3 w-3 text-muted-foreground hover:text-primary" />
</button>
<button
onClick={() => handleDeleteFact(k, translateFactKey(k))}
className="opacity-0 transition-opacity group-hover:opacity-100"
aria-label={`删除${translateFactKey(k)}`}
>
<Trash2 className="h-3 w-3 text-muted-foreground hover:text-red-500" />
</button>
</div>
</div>
))}
</div>
</div>
)}
{/* 无画像特征时也显示添加按钮 */}
{profileFacts.length === 0 && (
<div className="border-b pb-2">
<button
onClick={() => setAddingFact(true)}
className="flex items-center gap-0.5 text-xs text-primary hover:text-primary/80"
>
<Plus className="h-3 w-3" />添加画像特征
</button>
</div>
)}
{/* 个案事实:折叠展示 */}
{caseFacts.length > 0 && (
<div className="border-b pb-2">
<button
onClick={() => setShowCaseFacts(!showCaseFacts)}
className="mb-1 flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
<ChevronDown className={cn('h-3 w-3 transition-transform', !showCaseFacts && '-rotate-90')} />
个案事实({caseFacts.length})
</button>
{showCaseFacts && (
<div className="grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-3">
{caseFacts.map(([k, v]) => (
<div key={k} className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">{translateFactKey(k)}</span>
<span className="font-medium">{translateFactValue(v)}</span>
</div>
))}
</div>
)}
</div>
)}
</>
)
})()}
{profile.common_scenes && profile.common_scenes.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">常用场景</span>
<div className="flex flex-wrap gap-1">
{profile.common_scenes.map((s, i) => (
<Badge key={i} variant="outline" className="text-[10px] px-1 py-0">
{SCENE_LABELS[s] || s}
</Badge>
))}
</div>
</div>
)}
{profile.last_active && (
<p className="text-xs text-muted-foreground">
最后活跃:{formatTime(profile.last_active)}
</p>
)}
</div>
</CardContent>
</Card>
{/* 版本历史 */}
<div>
<div className="mb-2 flex items-center gap-2 px-1">
<History className="h-4 w-4 text-muted-foreground" aria-hidden />
<h3 className="text-sm font-semibold">版本历史</h3>
<span className="text-xs text-muted-foreground">最近 3 个版本</span>
</div>
{history.length === 0 ? (
<EmptyState
title="暂无历史版本"
description="每次画像更新都会记录版本快照"
icon={History}
/>
) : (
<div className="space-y-2">
{history.slice(0, 3).map((h) => (
<Card
key={h.id}
className="cursor-pointer transition-colors hover:bg-accent/50"
onClick={() => setCurrentHistory(h)}
>
<CardContent className="flex items-center gap-3 p-4">
<div className={cn(
'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',
h.source === 'manual' ? 'bg-blue-100 text-blue-700' : 'bg-muted text-muted-foreground',
)}>
<History className="h-4 w-4" />
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">版本 v{h.version}</p>
<div className="mt-0.5 flex items-center gap-2">
<Badge
variant={h.source === 'manual' ? 'default' : 'secondary'}
className="text-[10px]"
>
{SOURCE_LABELS[h.source] || h.source}
</Badge>
<span className="text-xs text-muted-foreground">{formatTime(h.created_at)}</span>
</div>
</div>
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
</CardContent>
</Card>
))}
</div>
)}
</div>
{/* 编辑弹窗 */}
<Dialog open={!!editing} onOpenChange={(open) => !open && setEditing(null)}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>编辑{editing?.label}</DialogTitle>
<DialogDescription>修改后将保存为新的画像版本</DialogDescription>
</DialogHeader>
{editing?.type === 'select' && editing.field in FIELD_OPTIONS ? (
<Select value={editValue} onValueChange={setEditValue}>
<SelectTrigger>
<SelectValue placeholder="请选择" />
</SelectTrigger>
<SelectContent>
{FIELD_OPTIONS[editing.field].map((opt) => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
placeholder={`请输入${editing?.label}`}
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
/>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setEditing(null)}>取消</Button>
<Button onClick={handleSave} disabled={saving || !editValue.trim()}>
{saving ? '保存中...' : '保存'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 添加画像特征弹窗 */}
<Dialog open={addingFact} onOpenChange={setAddingFact}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>添加画像特征</DialogTitle>
<DialogDescription>填写特征名称和值,保存后生效</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div>
<label className="mb-1 block text-xs text-muted-foreground">特征名称</label>
<Input
value={newFactKey}
onChange={(e) => setNewFactKey(e.target.value)}
placeholder="如:行业、公司规模、地区"
/>
</div>
<div>
<label className="mb-1 block text-xs text-muted-foreground">特征值</label>
<Input
value={newFactValue}
onChange={(e) => setNewFactValue(e.target.value)}
placeholder="如:科技、小型、上海"
onKeyDown={(e) => e.key === 'Enter' && handleAddFact()}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setAddingFact(false)}>取消</Button>
<Button onClick={handleAddFact} disabled={saving || !newFactKey.trim() || !newFactValue.trim()}>
{saving ? '保存中...' : '添加'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 删除确认弹窗 */}
<Dialog open={!!deletingFact} onOpenChange={(open) => !open && setDeletingFact(null)}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>删除画像特征</DialogTitle>
<DialogDescription>
确定要删除「{deletingFact?.label}」吗?此操作不可撤销。
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeletingFact(null)}>取消</Button>
<Button variant="destructive" onClick={confirmDeleteFact}>删除</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</PageLayout>
)
}