import { Alert, Button, Card, Collapse, Descriptions, Drawer, Select, Space, Table, Tag, Typography } from 'antd'; import { useEffect, useRef, useState } from 'react'; import { api } from '../api'; import type { BusinessBrief, DemandAnalysis, DemandModelConfig, DemandUsage, DiscoveryReading, PageResult } from '../types'; import { AnalysisReviewPanel, SourceReviewControl } from './AnalysisReviewPanel'; import { BriefConditions, RegionScope } from './BusinessBriefPanel'; import { ConsultationDetails } from './ConsultationDetails'; import { formatTime, NoRows } from './common'; const demandLabels: Record = { explicit: '明确需求', possible: '疑似需求', discussion: '普通讨论', insufficient: '信息不足' }; const missingLabels: Record = { identity_unconfirmed: '身份未核实', region_missing: '地区未提供', budget_missing: '预算未提供', stage_missing: '装修阶段未提供', media_uninterpreted: '媒体内容未解释', reading_partial: '阅读仅部分完成' }; const errorLabels: Record = { MODEL_TIMEOUT: '模型请求超时', MODEL_NETWORK: '模型连接未完成', MODEL_AUTH: '模型认证未通过', MODEL_RATE_LIMIT: '模型请求受限', MODEL_QUOTA: '模型额度不足', MODEL_UNAVAILABLE: '模型服务暂时不可用', MODEL_HTTP_ERROR: '模型服务未完成请求', MODEL_RESPONSE_INVALID: '模型响应结构未通过检查', MODEL_OUTPUT_INVALID: '需求结果或原文依据未通过检查', MODEL_RESPONSE_TOO_LARGE: '模型响应超出本次上限', MODEL_NOT_CONFIGURED: '服务端模型尚未配置', }; const roleLabels: Record = { suspected_demand_side: '疑似需求方', suspected_service_provider: '疑似服务方', participant: '普通参与者', unknown: '角色未知' }; const expressionLabels: Record = { self_need: '自身需求', question: '询问', seek_advice: '寻求建议', service_offer: '提供服务', advice: '给出建议', case_share: '案例分享', other: '其他表达', unknown: '表达不明' }; const matchLabels: Record = { matched: '条件初步匹配', possible: '可能匹配', insufficient: '条件不足', not_matched: '不匹配' }; const nextLabels: Record = { review_author_need: '核对作者自身需求', review_comment_need: '核对本条评论者需求', review_consultation: '核对咨询意向与实际需求', inspect_comments: '查看评论中的需求信号', need_more_context: '补充上下文后再判断', skip: '暂不列入跟进' }; const matchReasonLabels: Record = { self_need_explicit: '有明确自身需求信号', self_need_possible: '有疑似自身需求信号', no_self_need_signal: '没有自身需求信号', service_relevant: '与服务相关', service_irrelevant: '与服务不相关', service_unclear: '服务相关性不明', region_unspecified: '商家地区未设置', region_missing: '本条原文未提供地区', region_exact: '原文地区与条件字面一致', region_unrestricted: '商家已选择不限地区', region_potential_mismatch: '地区表述可能不匹配,需核对', service_promotion_excluded: '按条件排除服务推广', media_uninterpreted: '媒体内容未解释', reading_partial: '仅有部分阅读结果' }; function Quotes({ values }: { values: string[] }) { return <>{values.length ? values.map((value, index) => 「{value}」{index + 1 < values.length ? ';' : ''}) : '无原文依据,保留未知'}; } function safeError(code?: string | null) { return code ? errorLabels[code] || '模型请求或结果未通过检查' : '未提供错误分类'; } function sourceHref(value: string) { try { const url = new URL(value); return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password ? url.href : undefined; } catch { return undefined; } } function tokens(value: number | null | undefined) { return value === null || value === undefined ? '未知' : value.toLocaleString('zh-CN'); } function Usage({ value }: { value: DemandUsage | null }) { return 输入 Token:{tokens(value?.prompt_tokens)}输出 Token:{tokens(value?.completion_tokens)}总 Token:{tokens(value?.total_tokens)}缓存命中:{tokens(value?.prompt_cache_hit_tokens)}缓存未命中:{tokens(value?.prompt_cache_miss_tokens)}; } function ResultDetails({ analysis, reading }: { analysis: DemandAnalysis; reading: DiscoveryReading }) { return
{analysis.status === 'completed' ? '分析完成' : '分析未完成'}{analysis.prompt_version}{formatTime(analysis.created_at)} {analysis.status === 'failed' && } 本次纳入 {analysis.coverage.source_count} 个来源,仅核对已保存的文字;媒体未解释,身份待核实。 {analysis.context &&

本次商家条件

{analysis.context.conditions.service} · {analysis.context.conditions.target_customer} · {analysis.context.kind === 'confirmed_business_brief' ? '使用已确认商家需求;分析绑定该版本,不随当前表单改变。' : '使用本次初筛条件;旧记录未额外确认商家需求。'}{analysis.context.conditions.exclude_service_promotions ? ' 匹配时排除服务推广信号。' : ''}
}

逐项需求与原文依据

{analysis.status !== 'completed' ? : analysis.results.map(result => { const source = analysis.sources.find(item => item.source_ref === result.source_ref); if (!source) return null; return
{source.kind === 'note' ? '笔记正文' : `首层评论 ${source.comment_index ?? ''}`}{demandLabels[result.demand]}{result.media_uninterpreted && 媒体内容未解释} {source.comment_id && 本条评论标识:{source.comment_id}} 已读原文:{source.text || '无可读文字,媒体内容未解释。'} {result.role &&
{roleLabels[result.role]}{result.expressions?.map((expression, index) => {expressionLabels[expression.type]})}{result.match && {matchLabels[result.match]}} 角色依据: {result.expressions?.map((expression, index) => {expressionLabels[expression.type]}依据:)} {result.business_relevance && 服务相关性:{{ relevant: '相关', irrelevant: '不相关', unclear: '不明确' }[result.business_relevance.value]} · } {result.match_reasons?.length ? 匹配理由:{result.match_reasons.map(value => matchReasonLabels[value] || '条件待核对').join(';')} : null} {result.next_step && }
} 需求依据:{result.evidence.length ? result.evidence.map((quote, index) => 「{quote}」{index + 1 < result.evidence.length ? ';' : ''}) : '没有可核对的需求原文片段'} ({ key: field, label: { stage: '装修阶段', region: '地区', budget: '预算' }[field], children: result[field] ? <>{result[field].value} · 原文「{result[field].evidence}」 : '未提供,未推断' }))} /> {result.missing_information.map(item => {missingLabels[item] || '信息待核实'})} 来源:本次已保存的{source.kind === 'note' ? '笔记正文' : '首层评论'}。{sourceHref(reading.note_url) && 查看笔记页面}
; })}
{analysis.id} }, { key: 'time', label: '分析时间', children: formatTime(analysis.created_at) }, { key: 'model', label: '本次配置型号', children: analysis.config.model }, { key: 'provider', label: '供应商', children: analysis.config.provider }, { key: 'duration', label: '分析耗时', children: `${(analysis.duration_ms / 1000).toFixed(2)} 秒` }, { key: 'range', label: '实际分析范围', children: `${analysis.coverage.source_count} 个来源,${analysis.coverage.text_chars} 字` }, { key: 'bounded', label: '原阅读范围', children: analysis.coverage.reading_truncated ? '已截断,仅判断已读内容' : '本次范围未截断' }, { key: 'comments', label: '评论', children: analysis.coverage.comments_available ? '有已读评论' : '未取得,不代表没有评论' }, { key: 'price', label: '供应商费用', children: '未知(未接价格结算)' }, { key: 'versions', label: '规则 / 结果版本', children: `${analysis.prompt_version} / ${analysis.result_version}` }, ]} />

实际 Token 用量

仅汇总已返回用量;失败或超时未返回的用量未知,未计入汇总,费用未结算。

主备请求记录

{analysis.attempts.length ?
{analysis.attempts.map((attempt, index) =>
{attempt.slot === 'primary' ? '主密钥' : '备用密钥'}{attempt.status === 'succeeded' ? '请求成功' : '请求未通过'}{(attempt.duration_ms / 1000).toFixed(2)} 秒{attempt.http_status !== null && HTTP {attempt.http_status}} {attempt.response_model && 实际返回型号:{attempt.response_model}} {attempt.status === 'failed' && {safeError(attempt.error_code)}}
)}
: 没有模型调用记录。}
阅读内容摘要:{analysis.reading_digest}
模型配置摘要:{analysis.config.config_digest}
}]} />
; } export function ReadingDemandAnalysis({ reading }: { reading: DiscoveryReading }) { const [config, setConfig] = useState(); const [briefs, setBriefs] = useState([]); const [briefId, setBriefId] = useState(reading.business_brief_id || ''); const [history, setHistory] = useState([]); const [selectedId, setSelectedId] = useState(''); const [detailsOpen, setDetailsOpen] = useState(false); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [loadError, setLoadError] = useState(false); const [requestError, setRequestError] = useState(false); const [reused, setReused] = useState(false); const active = useRef(false); const mounted = useRef(true); const generation = useRef(0); const route = `/discovery/readings/${encodeURIComponent(reading.id)}/analyses`; useEffect(() => { mounted.current = true; return () => { mounted.current = false; generation.current += 1; }; }, []); async function refresh() { const current = ++generation.current; setLoading(true); setLoadError(false); const [status, records, business] = await Promise.allSettled([api.get('/analysis/config'), api.get>(route), api.get>('/business/briefs')]); if (!mounted.current || current !== generation.current) return; if (status.status === 'fulfilled') setConfig(status.value); else setConfig(undefined); if (records.status === 'fulfilled') { const items = records.value.items.filter(item => item.reading_id === reading.id && item.discovery_run_id === reading.discovery_run_id); setHistory(items); setSelectedId(previous => items.some(item => item.id === previous) ? previous : items[0]?.id || ''); } if (business.status === 'fulfilled') setBriefs(business.value.items.filter(item => item.status === 'confirmed')); setLoadError(status.status === 'rejected' || records.status === 'rejected'); setLoading(false); } useEffect(() => { void refresh(); }, [reading.id]); const binding = reading.note_binding_result; const eligible = reading.status !== 'failed' && Boolean(reading.detail?.items.length && binding?.href_checked && binding.url_checked && binding.note_id === reading.note_id); async function analyze() { if (active.current || !config?.configured || !eligible) return; active.current = true; setBusy(true); setRequestError(false); setReused(false); try { const result = await api.post(route, briefId ? { business_brief_id: briefId } : {}); if (!mounted.current) return; setReused(history.some(item => item.id === result.id)); await refresh(); if (mounted.current && result.reading_id === reading.id && result.discovery_run_id === reading.discovery_run_id) { setSelectedId(result.id); setDetailsOpen(true); } } catch { if (mounted.current) { setRequestError(true); await refresh(); } } finally { active.current = false; if (mounted.current) setBusy(false); } } const selected = history.find(item => item.id === selectedId); return void refresh()}>刷新分析记录}>
{loadError && }

当前服务器模型配置

{config ? <> {config.configured ? '已配置' : '尚未配置'}{config.provider || '供应商未设置'}{config.model || '型号未设置'}主密钥:{config.key_slots.includes('primary') ? '已配置' : '未配置'}独立备用:{config.key_slots.includes('fallback') ? '已配置' : '未启用'} 已配置不等于已连通。这里只显示配置状态;相同密钥会去重为一个槽位。备用仅对受支持的临时失败有限尝试,本轮不提供密钥输入或查看。 : 尚未取得安全配置状态。}
{!eligible && } {requestError && }

分析所用商家条件