feat(ssa): Complete Phase V-A editable analysis plan variables
Features: - Add editable variable selection in workflow plan (SingleVarSelect + MultiVarTags) - Implement 3-layer flexible interception (warning bar + icon + blocking dialog) - Add tool_param_constraints.json for 12 statistical tools parameter validation - Add PATCH /workflow/:id/params API with Zod structural validation - Implement synchronous parameter sync before execution (Promise chaining) - Fix LLM hallucination by strict system prompt constraints - Fix DynamicReport object-based rows compatibility (R baseline_table) - Fix Word export row.map error with same normalization logic - Restore inferGroupingVar for smart default variable selection - Add ReactMarkdown rendering in SSAChatPane - Update SSA module status document to v3.5 Modified files: - backend: workflow.routes, ChatHandlerService, SystemPromptService, FlowTemplateService - frontend: WorkflowTimeline, SSAWorkspacePane, DynamicReport, SSAChatPane, ssaStore, ssa.css - config: tool_param_constraints.json (new) - docs: SSA status doc, team review reports Tested: Cohort study end-to-end execution + report export verified Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -239,18 +239,20 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
}
|
||||
|
||||
// analysis_plan 事件(Phase IV: 对话驱动分析)
|
||||
// 仅创建记录,不打开工作区 — 等用户确认方案后再打开
|
||||
if (parsed.type === 'analysis_plan' && parsed.plan) {
|
||||
const plan = parsed.plan as WorkflowPlan;
|
||||
const { addRecord, setActivePane, setWorkspaceOpen } = useSSAStore.getState();
|
||||
const { addRecord } = useSSAStore.getState();
|
||||
addRecord(content, plan);
|
||||
setActivePane('sap');
|
||||
setWorkspaceOpen(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// plan_confirmed 事件(Phase IV: 用户确认方案后触发执行)
|
||||
if (parsed.type === 'plan_confirmed' && parsed.workflowId) {
|
||||
setPendingPlanConfirm({ workflowId: parsed.workflowId });
|
||||
// plan_confirmed 事件(Phase IV: 用户确认方案后打开工作区)
|
||||
// 不自动触发 executeWorkflow — 由用户在工作区手动点击「开始执行分析」
|
||||
if (parsed.type === 'plan_confirmed') {
|
||||
const { setActivePane, setWorkspaceOpen } = useSSAStore.getState();
|
||||
setActivePane('sap');
|
||||
setWorkspaceOpen(true);
|
||||
setPendingQuestion(null);
|
||||
continue;
|
||||
}
|
||||
@@ -319,14 +321,27 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
|
||||
/**
|
||||
* 响应 ask_user 卡片(Phase III)
|
||||
* 将 value 解析为中文 label 用于显示
|
||||
*/
|
||||
const respondToQuestion = useCallback(async (sessionId: string, response: AskUserResponseData) => {
|
||||
const question = pendingQuestion;
|
||||
setPendingQuestion(null);
|
||||
const displayText = response.action === 'select'
|
||||
? `选择了: ${response.selectedValues?.join(', ')}`
|
||||
: response.freeText || '(已回复)';
|
||||
|
||||
let displayText: string;
|
||||
if (response.action === 'select' && response.selectedValues) {
|
||||
const labels = response.selectedValues.map(val => {
|
||||
const opt = question?.options?.find(o => o.value === val);
|
||||
return opt?.label || val;
|
||||
});
|
||||
displayText = `选择了 ${labels.join('、')}`;
|
||||
} else if (response.action === 'free_text') {
|
||||
displayText = response.freeText || '(已回复)';
|
||||
} else {
|
||||
displayText = '(已回复)';
|
||||
}
|
||||
|
||||
await sendChatMessage(sessionId, displayText, { askUserResponse: response });
|
||||
}, [sendChatMessage]);
|
||||
}, [sendChatMessage, pendingQuestion]);
|
||||
|
||||
/**
|
||||
* H1: 跳过 ask_user 卡片
|
||||
@@ -337,7 +352,7 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
questionId,
|
||||
action: 'skip',
|
||||
};
|
||||
await sendChatMessage(sessionId, '跳过了此问题', { askUserResponse: skipResponse });
|
||||
await sendChatMessage(sessionId, '已跳过此问题', { askUserResponse: skipResponse });
|
||||
}, [sendChatMessage]);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user