feat(ssa): Complete QPER architecture - Query, Planner, Execute, Reflection layers

Implement the full QPER intelligent analysis pipeline:

- Phase E+: Block-based standardization for all 7 R tools, DynamicReport renderer, Word export enhancement

- Phase Q: LLM intent parsing with dynamic Zod validation against real column names, ClarificationCard component, DataProfile is_id_like tagging

- Phase P: ConfigLoader with Zod schema validation and hot-reload API, DecisionTableService (4-dimension matching), FlowTemplateService with EPV protection, PlannedTrace audit output

- Phase R: ReflectionService with statistical slot injection, sensitivity analysis conflict rules, ConclusionReport with section reveal animation, conclusion caching API, graceful R error classification

End-to-end test: 40/40 passed across two complete analysis scenarios.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-21 18:15:53 +08:00
parent 428a22adf2
commit 371e1c069c
73 changed files with 9242 additions and 706 deletions

View File

@@ -29,6 +29,8 @@ import { useWorkflow } from '../hooks/useWorkflow';
import type { SSAMessage } from '../types';
import { TypeWriter } from './TypeWriter';
import { DataProfileCard } from './DataProfileCard';
import { ClarificationCard } from './ClarificationCard';
import type { ClarificationCardData, IntentResult } from '../types';
export const SSAChatPane: React.FC = () => {
const navigate = useNavigate();
@@ -46,15 +48,21 @@ export const SSAChatPane: React.FC = () => {
error,
setError,
addToast,
addMessage,
selectAnalysisRecord,
dataProfile,
dataProfileLoading,
} = useSSAStore();
const { uploadData, generatePlan, isUploading, uploadProgress } = useAnalysis();
const { generateDataProfile, generateWorkflowPlan, isProfileLoading, isPlanLoading } = useWorkflow();
const { generateDataProfile, generateWorkflowPlan, parseIntent, handleClarify, isProfileLoading, isPlanLoading } = useWorkflow();
const [inputValue, setInputValue] = useState('');
const [uploadStatus, setUploadStatus] = useState<'idle' | 'uploading' | 'parsing' | 'success' | 'error'>('idle');
const [pendingClarification, setPendingClarification] = useState<{
cards: ClarificationCardData[];
originalQuery: string;
intent: IntentResult;
} | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const chatEndRef = useRef<HTMLDivElement>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null);
@@ -143,20 +151,83 @@ export const SSAChatPane: React.FC = () => {
const handleSend = async () => {
if (!inputValue.trim()) return;
const query = inputValue;
setInputValue('');
try {
// Phase 2A: 如果已有 session使用多步骤工作流规划
if (currentSession?.id) {
await generateWorkflowPlan(currentSession.id, inputValue);
// Phase Q: 先做意图解析,低置信度时追问
const intentResp = await parseIntent(currentSession.id, query);
if (intentResp.needsClarification && intentResp.clarificationCards?.length > 0) {
addMessage({
id: crypto.randomUUID(),
role: 'user',
content: query,
createdAt: new Date().toISOString(),
});
addMessage({
id: crypto.randomUUID(),
role: 'assistant',
content: `我大致理解了你的意图(${intentResp.intent.reasoning}),但为了生成更精确的分析方案,想确认几个细节:`,
createdAt: new Date().toISOString(),
});
setPendingClarification({
cards: intentResp.clarificationCards,
originalQuery: query,
intent: intentResp.intent,
});
return;
}
// 置信度足够 → 直接生成工作流计划
await generateWorkflowPlan(currentSession.id, query);
} else {
// 没有数据时,使用旧流程
await generatePlan(inputValue);
await generatePlan(query);
}
setInputValue('');
} catch (err: any) {
addToast(err?.message || '生成计划失败', 'error');
}
};
const handleClarificationSelect = async (selections: Record<string, string>) => {
if (!currentSession?.id || !pendingClarification) return;
setPendingClarification(null);
const selectedLabel = Object.values(selections).join(', ');
addMessage({
id: crypto.randomUUID(),
role: 'user',
content: selectedLabel,
createdAt: new Date().toISOString(),
});
try {
const resp = await handleClarify(
currentSession.id,
pendingClarification.originalQuery,
selections
);
if (resp.needsClarification && resp.clarificationCards?.length > 0) {
addMessage({
id: crypto.randomUUID(),
role: 'assistant',
content: '还需要确认一下:',
createdAt: new Date().toISOString(),
});
setPendingClarification({
cards: resp.clarificationCards,
originalQuery: pendingClarification.originalQuery,
intent: resp.intent,
});
}
} catch (err: any) {
addToast(err?.message || '处理追问失败', 'error');
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@@ -308,6 +379,20 @@ export const SSAChatPane: React.FC = () => {
旧的"数据挂载成功消息"已移除,避免在没有用户输入时就显示 SAP 卡片
*/}
{/* Phase Q: 追问卡片 */}
{pendingClarification && (
<div className="message-row assistant">
<div className="avatar-col"><Bot size={18} /></div>
<div className="msg-content">
<ClarificationCard
cards={pendingClarification.cards}
onSelect={handleClarificationSelect}
disabled={isLoading}
/>
</div>
</div>
)}
{/* 自动滚动锚点与占位垫片 - 解决底部输入框遮挡的终极方案 */}
<div ref={chatEndRef} className="scroll-spacer" />
</div>