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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user