feat(ai-analysis): 在AI助手展示业务分析结果
This commit is contained in:
37
apps/web/components/analysis/AnalysisReport.tsx
Normal file
37
apps/web/components/analysis/AnalysisReport.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { AnalysisReport as AnalysisReportData } from '@ftb/shared';
|
||||
import { EvidenceList } from './EvidenceList';
|
||||
|
||||
export function AnalysisReport({ report }: { report: AnalysisReportData }) {
|
||||
return (
|
||||
<section className="rounded-[28px] border border-white/60 bg-white/75 p-5 shadow-[0_18px_60px_rgba(15,23,42,0.08)] backdrop-blur-xl">
|
||||
<h3 className="text-[14px] font-semibold text-[#111827]">分析报告</h3>
|
||||
<p className="mt-3 text-[14px] leading-7 text-[#334155]">{report.summary}</p>
|
||||
<ReportSection title="关键发现" items={report.keyFindings} />
|
||||
<div className="mt-4">
|
||||
<p className="mb-2 text-[12px] font-medium text-[#64748b]">数据依据</p>
|
||||
<EvidenceList items={report.evidence} />
|
||||
</div>
|
||||
<ReportSection title="建议动作" items={report.suggestions} />
|
||||
<div className="mt-4 rounded-2xl bg-[#f8fafc] p-3 text-[12px] leading-6 text-[#64748b]">
|
||||
<p>{report.dataScope.timeDescription}</p>
|
||||
<p>{report.dataScope.permissionDescription}</p>
|
||||
<p>{report.dataScope.metricFormulaDescription}</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ReportSection({ title, items }: { title: string; items: string[] }) {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<p className="mb-2 text-[12px] font-medium text-[#64748b]">{title}</p>
|
||||
<ul className="space-y-1.5 text-[13px] leading-6 text-[#334155]">
|
||||
{items.map((item) => (
|
||||
<li key={item}>• {item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
apps/web/components/analysis/AnalysisResultBlock.tsx
Normal file
44
apps/web/components/analysis/AnalysisResultBlock.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { AnalysisResponse } from '@ftb/shared';
|
||||
import { AnalysisChart } from './AnalysisChart';
|
||||
import { AnalysisReport } from './AnalysisReport';
|
||||
import { FollowUpActions } from './FollowUpActions';
|
||||
import { InsightCard } from './InsightCard';
|
||||
|
||||
export function AnalysisResultBlock({
|
||||
response,
|
||||
onAsk,
|
||||
}: {
|
||||
response: AnalysisResponse;
|
||||
onAsk: (prompt: string) => void;
|
||||
}) {
|
||||
if (!response.ok) {
|
||||
return (
|
||||
<div className="rounded-[28px] border border-[#e2e8f0] bg-white/80 p-5 text-[14px] leading-7 text-[#334155] shadow-sm backdrop-blur-xl">
|
||||
<p className="font-medium text-[#0f172a]">{response.message}</p>
|
||||
{response.clarificationOptions && (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{response.clarificationOptions.map((option) => (
|
||||
<button
|
||||
key={option.prompt}
|
||||
type="button"
|
||||
onClick={() => onAsk(option.prompt)}
|
||||
className="rounded-full border border-[#dbe3ef] bg-white/70 px-3 py-2 text-[13px] text-[#334155] hover:bg-white"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<InsightCard insight={response.insight} />
|
||||
<AnalysisChart spec={response.chart} />
|
||||
<AnalysisReport report={response.report} />
|
||||
<FollowUpActions followUps={response.followUps} onAsk={onAsk} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
apps/web/components/analysis/EvidenceList.tsx
Normal file
25
apps/web/components/analysis/EvidenceList.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { EvidenceItem } from '@ftb/shared';
|
||||
|
||||
export function EvidenceList({ items }: { items: EvidenceItem[] }) {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={`${item.label}-${index}`}
|
||||
type="button"
|
||||
className="rounded-full border border-[#e2e8f0] bg-white/70 px-3 py-1.5 text-[12px] text-[#334155] shadow-sm backdrop-blur disabled:cursor-default"
|
||||
disabled={!item.drilldown}
|
||||
title={item.sourceLabel ?? item.sourceDomain}
|
||||
>
|
||||
<span className="text-[#64748b]">{item.label}</span>
|
||||
<span className="ml-1 font-semibold text-[#0f172a]">
|
||||
{item.value}
|
||||
{item.unit ?? ''}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
apps/web/components/analysis/FollowUpActions.tsx
Normal file
23
apps/web/components/analysis/FollowUpActions.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { FollowUp } from '@ftb/shared';
|
||||
|
||||
export function FollowUpActions({ followUps, onAsk }: { followUps: FollowUp[]; onAsk: (prompt: string) => void }) {
|
||||
if (followUps.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{followUps.map((item) => (
|
||||
<button
|
||||
key={`${item.type}-${item.label}`}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (item.type === 'question') onAsk(item.prompt);
|
||||
}}
|
||||
className="rounded-full border border-[#dbe3ef] bg-white/70 px-3 py-2 text-[13px] text-[#334155] shadow-sm transition-colors hover:bg-white disabled:cursor-default disabled:opacity-60"
|
||||
disabled={item.type !== 'question'}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
apps/web/components/analysis/InsightCard.tsx
Normal file
31
apps/web/components/analysis/InsightCard.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { InsightCard as InsightCardData } from '@ftb/shared';
|
||||
|
||||
export function InsightCard({ insight }: { insight: InsightCardData }) {
|
||||
return (
|
||||
<section className="rounded-[28px] border border-white/60 bg-white/80 p-5 shadow-[0_18px_60px_rgba(15,23,42,0.08)] backdrop-blur-xl">
|
||||
{insight.primaryValue && (
|
||||
<div className="mb-3">
|
||||
<div className="text-[44px] font-semibold leading-none tracking-normal text-[#0f172a]">
|
||||
{insight.primaryValue.value}
|
||||
{insight.primaryValue.unit ?? ''}
|
||||
</div>
|
||||
<div className="mt-2 text-[13px] text-[#64748b]">{insight.primaryValue.label}</div>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-[15px] leading-7 text-[#111827]">{insight.summary}</p>
|
||||
{(insight.semanticConfidence !== 'high' || insight.dataConfidence !== 'sufficient') && (
|
||||
<p className="mt-3 text-[12px] text-[#64748b]">
|
||||
语义置信:{confidenceLabel(insight.semanticConfidence)} · 数据充分性:{dataLabel(insight.dataConfidence)}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function confidenceLabel(value: InsightCardData['semanticConfidence']) {
|
||||
return value === 'high' ? '高' : value === 'medium' ? '中' : '低';
|
||||
}
|
||||
|
||||
function dataLabel(value: InsightCardData['dataConfidence']) {
|
||||
return value === 'sufficient' ? '数据充分' : value === 'partial' ? '部分数据' : '数据不足';
|
||||
}
|
||||
Reference in New Issue
Block a user