@@ -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")