From 06c59fdcb7495ec8d9b9425baf5e0d08421389bf Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Thu, 16 Jul 2026 18:27:06 +0800 Subject: [PATCH] Add active rectification web wizard --- jyotish-app/rectification.js | 126 ++++++++++++++++++++++++++ tests/test_frontend_productization.py | 27 ++++++ 2 files changed, 153 insertions(+) diff --git a/jyotish-app/rectification.js b/jyotish-app/rectification.js index 565a43fe..72272fd0 100644 --- a/jyotish-app/rectification.js +++ b/jyotish-app/rectification.js @@ -16,6 +16,9 @@ function fmtOffset(m) { return m === 0 ? t('rect.baseline') : `${m > 0 ? '+' : ' let rectEvents = []; let rectInterviewAnswers = {}; let rectRecommendedEvents = []; +let activeRectificationQuestionnaire = null; +let activeRectificationAnswers = {}; +let activeRectificationScore = null; export function renderRectificationTab(container) { const lang = getLang(); @@ -56,6 +59,27 @@ export function renderRectificationTab(container) { +
+
+
+

主动问询式校时

+

系统先按出生时间误差生成高信息量选择题;你只需点选答案,再进入下一轮收敛。

+
+ active_rectification_questions · candidate_cluster_scoring +
+
+
+
+
+ +
+
+
+
+ +
+
+
@@ -110,6 +134,7 @@ function pctStyle(value) { function bindEvents(container) { const q = s => container.querySelector(s); bindInterviewEvents(container); + bindActiveRectificationWizard(container); q('#rect-add-btn').addEventListener('click', () => { const dEl = q('#rect-event-date'), cEl = q('#rect-event-cat'), descEl = q('#rect-event-desc'); if (!dEl.value) { dEl.focus(); return; } @@ -170,6 +195,107 @@ function bindInterviewEvents(container) { }); } +function toApiBirthTime(value) { + return value ? value.replace('T', ' ').slice(0, 16) : ''; +} + +function renderActiveRectificationQuestions(container) { + const target = container.querySelector('#rect-active-questions'); + if (!target || !activeRectificationQuestionnaire) return; + target.innerHTML = (activeRectificationQuestionnaire.questions || []).map(question => ` +
+
+ ${escapeHtml(question.prompt)} + ${escapeHtml((question.sensitivity || []).join(' / '))} · ${escapeHtml(question.window || '')} +
+
+ ${(question.options || []).map(option => ` + + `).join('')} +
+
+ `).join(''); + target.querySelectorAll('[data-active-answer]').forEach(button => { + button.addEventListener('click', () => { + const item = button.closest('[data-active-question-id]'); + if (!item) return; + activeRectificationAnswers[item.dataset.activeQuestionId] = button.dataset.activeAnswer; + item.querySelectorAll('[data-active-answer]').forEach(btn => btn.classList.remove('selected')); + button.classList.add('selected'); + }); + }); +} + +function renderActiveRectificationScore(container) { + const target = container.querySelector('#rect-active-score-result'); + if (!target || !activeRectificationScore) return; + const rankings = activeRectificationScore.candidate_cluster_rankings || []; + const nextQuestions = activeRectificationScore.next_round_questions || []; + target.innerHTML = ` +
+
候选时间簇
+
${rankings.map(row => ` +
${escapeHtml(row.cluster)}${escapeHtml(String(row.score))}
+ `).join('') || '暂无评分'}
+
下一轮优先问题
+
${nextQuestions.map(q => ` +
${escapeHtml(q.domain || q.id)}${escapeHtml(q.prompt || '')}
+ `).join('') || '暂无下一轮问题'}
+
+ `; +} + +function bindActiveRectificationWizard(container) { + const status = container.querySelector('#rect-active-status'); + container.querySelector('#rect-active-load')?.addEventListener('click', async () => { + const birthTime = toApiBirthTime(container.querySelector('#rect-active-birth-time')?.value || ''); + if (!birthTime) { + if (status) status.textContent = '请先填写出生时间中心。'; + return; + } + if (!window.JyotishAPI?.computeActiveRectificationQuestions) { + if (status) status.textContent = '本地 API 尚未加载主动问询能力。'; + return; + } + try { + if (status) status.textContent = '正在生成高信息量问题…'; + activeRectificationQuestionnaire = await window.JyotishAPI.computeActiveRectificationQuestions({ + birth_time: birthTime, + uncertainty_minutes: Number(container.querySelector('#rect-active-uncertainty')?.value || 30), + step_minutes: Number(container.querySelector('#rect-active-step')?.value || 1), + }); + activeRectificationAnswers = {}; + activeRectificationScore = null; + renderActiveRectificationQuestions(container); + renderActiveRectificationScore(container); + if (status) status.textContent = `已生成 ${(activeRectificationQuestionnaire.questions || []).length} 个选择题。`; + } catch (error) { + if (status) status.textContent = error.message || '主动问询生成失败'; + } + }); + container.querySelector('#rect-active-score')?.addEventListener('click', async () => { + if (!activeRectificationQuestionnaire) { + if (status) status.textContent = '请先生成问题。'; + return; + } + if (!window.JyotishAPI?.computeActiveRectificationScore) { + if (status) status.textContent = '本地 API 尚未加载主动问询评分能力。'; + return; + } + try { + if (status) status.textContent = '正在评分并收敛候选…'; + activeRectificationScore = await window.JyotishAPI.computeActiveRectificationScore({ + questionnaire: activeRectificationQuestionnaire, + answers: activeRectificationAnswers, + }); + renderActiveRectificationScore(container); + if (status) status.textContent = `已评分 ${activeRectificationScore.answered_count || 0} 个回答。`; + } catch (error) { + if (status) status.textContent = error.message || '主动问询评分失败'; + } + }); +} + function collectInterviewEvents(container) { const answers = [...container.querySelectorAll('.rect-interview-item')].map(item => { const saved = rectInterviewAnswers[item.dataset.questionId] || {}; diff --git a/tests/test_frontend_productization.py b/tests/test_frontend_productization.py index c8cf3fc1..9b16d157 100644 --- a/tests/test_frontend_productization.py +++ b/tests/test_frontend_productization.py @@ -1615,6 +1615,33 @@ def test_rectification_ui_guides_yes_no_interview_from_existing_event_assets() - assert token in rect_ui +def test_rectification_ui_exposes_active_questionnaire_api_wizard() -> None: + rect_ui = read("rectification.js") + bridge = read("api-bridge.js") + public_bridge = read("public/api-bridge.js") + for token in [ + "rect-active-wizard", + "data-active-rectification-wizard", + "rect-active-birth-time", + "rect-active-uncertainty", + "rect-active-load", + "rect-active-score", + "computeActiveRectificationQuestions", + "computeActiveRectificationScore", + "active_rectification_questions", + "candidate_cluster_rankings", + "next_round_questions", + "data-active-answer", + "activeRectificationAnswers", + ]: + assert token in rect_ui + for source in (bridge, public_bridge): + assert "computeActiveRectificationQuestions" in source + assert "computeActiveRectificationScore" in source + assert "/api/active_rectification_questions" in source + assert "/api/active_rectification_score" in source + + def test_web_entry_prefers_unified_consultation_workflow() -> None: bridge = read("api-bridge.js") public_bridge = read("public/api-bridge.js")