Files
Jyotisha/jyotish-app/rectification.js
T
2026-07-01 19:15:35 +08:00

499 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 生时校正 UI 渲染层 v2.0
* 引擎逻辑在 rectification-engine.js
*/
import { SIGNS, PLANET_CN, SIGN_LORDS } from './jyotish-engine.js';
import {
EVENT_CATEGORIES, EVENT_COLLECTION_GUIDE, VARGA_SENSITIVITY, runRectification,
buildRectificationInterviewQuestions, buildRecommendedRectificationQuestions, rectificationInterviewAnswersToEvents,
getHouseLord, fmtTime, dateToJD
} from './rectification-engine.js';
import { t, getLang, signName, planetName } from './i18n.js';
import { escapeHtml, escapeAttr } from './security.js';
function fmtOffset(m) { return m === 0 ? t('rect.baseline') : `${m > 0 ? '+' : ''}${m}min`; }
let rectEvents = [];
let rectInterviewAnswers = {};
let rectRecommendedEvents = [];
export function renderRectificationTab(container) {
const lang = getLang();
container.innerHTML = `
<div class="rect-header">
<h3 class="section-title">${t('rect.title')}</h3>
<p class="rect-subtitle">${t('rect.subtitle')}</p>
<div class="rect-method-info">${t('rect.method.info')}</div>
</div>
<div class="rect-config card">
<h4 class="sub-title">${t('rect.config')}</h4>
<div class="rect-config-row">
<div class="form-group"><label>出生时间区间开始</label><input type="time" id="rect-window-start" step="60"></div>
<div class="form-group"><label>出生时间区间结束</label><input type="time" id="rect-window-end" step="60"></div>
<div class="form-group"><label>${t('rect.range')}</label>
<select id="rect-range">
<option value="5">${t('rect.range.5')}</option>
<option value="10">${t('rect.range.10')}</option>
<option value="15" selected>${t('rect.range.15')}</option>
<option value="20">${t('rect.range.20')}</option>
<option value="30">${t('rect.range.30')}</option>
</select></div>
<div class="form-group"><label>${t('rect.step')}</label>
<select id="rect-step">
<option value="0.5">${t('rect.step.30s')}</option>
<option value="1" selected>${t('rect.step.1m')}</option>
<option value="2">${t('rect.step.2m')}</option>
<option value="5">${t('rect.step.5m')}</option>
</select></div>
</div>
<details class="rect-sensitivity-details">
<summary>${t('rect.sensitivity.info')}</summary>
<table class="rect-table rect-sens-table">
<thead><tr><th>${t('rect.varga.col')}</th><th>${t('rect.time.window')}</th></tr></thead>
<tbody>${Object.entries(VARGA_SENSITIVITY).map(([k, v]) =>
`<tr><td>${k} (${lang === 'en' ? v.en : v.cn})</td><td>~${v.min} ${t('rect.min.unit')}</td></tr>`
).join('')}</tbody>
</table>
</details>
</div>
<div class="rect-interview card">
<div class="rect-interview-head">
<div>
<h4 class="sub-title">快速事件访谈</h4>
<p>只回答是/否;选“是”时补一个大概日期,系统会自动转成生命事件。</p>
</div>
<span>guided_rectification_interview · recommended_events</span>
</div>
<div class="rect-interview-list">${renderRectificationInterview(lang)}</div>
<div class="rect-interview-actions">
<button type="button" class="rect-secondary-btn" id="rect-import-interview">把回答加入事件并开始校正</button>
<span id="rect-interview-status" aria-live="polite"></span>
</div>
</div>
<div class="rect-events card">
<h4 class="sub-title">${t('rect.events')} <span class="rect-count" id="rect-event-count">0 ${t('rect.event.count')}</span></h4>
<p class="rect-hint">${t('rect.events.hint')}</p>
<div class="rect-event-guide">${EVENT_COLLECTION_GUIDE.map(group =>
`<span>${lang === 'en' ? escapeHtml(group.en) : escapeHtml(group.cn)}</span>`
).join('')}</div>
<div class="rect-add-row">
<div class="form-group"><label>${t('rect.event.date')}</label><input type="date" id="rect-event-date" required></div>
<div class="form-group"><label>${t('rect.event.cat')}</label>
<select id="rect-event-cat">${Object.entries(EVENT_CATEGORIES).map(([k, v]) =>
`<option value="${k}">${v.icon} ${lang === 'en' ? v.en : v.cn} (${v.varga})</option>`
).join('')}</select></div>
<div class="form-group"><label>${t('rect.event.desc')}</label><input type="text" id="rect-event-desc" placeholder="${t('rect.event.ph')}"></div>
<button class="btn-primary btn-sm" id="rect-add-btn">${t('rect.event.add')}</button>
</div>
<div id="rect-event-list" class="rect-event-list"></div>
</div>
<div class="rect-action">
<button class="btn-primary" id="rect-run-btn">
<span class="btn-text">${t('rect.run')}</span>
<span class="btn-loading hidden"><span class="spinner"></span> ${t('rect.computing')}</span>
</button>
</div>
<div id="rect-progress" class="rect-progress hidden">
<div class="rect-progress-bar"><div class="rect-progress-fill" id="rect-progress-fill"></div></div>
<p class="rect-progress-text" id="rect-progress-text">${t('rect.preparing')}</p>
</div>
<div id="rect-results" class="hidden"></div>`;
bindEvents(container);
}
function pctStyle(value) {
const num = Number(value);
const pct = Number.isFinite(num) ? Math.max(0, Math.min(100, Math.round(num))) : 0;
return `${pct}%`;
}
function bindEvents(container) {
const q = s => container.querySelector(s);
bindInterviewEvents(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; }
rectEvents.push({ date: dEl.value, category: cEl.value, desc: descEl.value || EVENT_CATEGORIES[cEl.value].cn });
dEl.value = ''; descEl.value = '';
renderEventList(container);
});
q('#rect-run-btn').addEventListener('click', function () { handleRun(container, this); });
}
function renderInterviewQuestion(question, lang) {
const prompt = lang === 'en' ? question.question_en : question.question_cn;
const label = lang === 'en' ? question.label_en : question.label_cn;
const examples = (question.examples_cn || []).join(' / ');
return `<div class="rect-interview-item" data-question-id="${escapeAttr(question.id)}" data-category="${escapeAttr(question.category)}">
<div class="rect-interview-copy">
<strong>${escapeHtml(prompt)}</strong>
<span>${escapeHtml(label)} · ${escapeHtml(question.varga)}${examples ? ` · ${escapeHtml(examples)}` : ''}</span>
</div>
<div class="rect-answer-group" role="group" aria-label="${escapeAttr(prompt)}">
<button type="button" data-rect-answer="yes">是</button>
<button type="button" data-rect-answer="no">否</button>
<button type="button" data-rect-answer="other">其他</button>
</div>
<div class="rect-answer-detail hidden">
<input type="date" class="rect-answer-date" aria-label="事件日期">
<input type="text" class="rect-answer-note" placeholder="补充说明,可选">
</div>
</div>`;
}
function bindInterviewEvents(container) {
container.querySelectorAll('.rect-interview-item').forEach(item => {
item.querySelectorAll('[data-rect-answer]').forEach(btn => {
btn.addEventListener('click', () => {
const answer = btn.dataset.rectAnswer;
item.querySelectorAll('[data-rect-answer]').forEach(other => other.classList.toggle('active', other === btn));
item.querySelector('.rect-answer-detail')?.classList.toggle('hidden', answer !== 'yes' && answer !== 'other');
rectInterviewAnswers[item.dataset.questionId] = {
answer,
category: item.dataset.category,
};
});
});
});
container.querySelector('#rect-import-interview')?.addEventListener('click', () => {
const events = collectInterviewEvents(container);
if (!events.length) {
const status = container.querySelector('#rect-interview-status');
if (status) status.textContent = '还没有可导入的“是”回答。';
return;
}
rectEvents = [...rectEvents, ...events];
renderEventList(container);
const status = container.querySelector('#rect-interview-status');
if (status) status.textContent = `已加入 ${events.length} 个事件,正在准备校正。`;
container.querySelector('#rect-run-btn')?.click();
});
}
function collectInterviewEvents(container) {
const answers = [...container.querySelectorAll('.rect-interview-item')].map(item => {
const saved = rectInterviewAnswers[item.dataset.questionId] || {};
return {
...saved,
category: saved.category || item.dataset.category,
date: item.querySelector('.rect-answer-date')?.value || '',
note: item.querySelector('.rect-answer-note')?.value || '',
};
});
return rectificationInterviewAnswersToEvents(answers);
}
function renderEventList(container) {
const listEl = container.querySelector('#rect-event-list');
const countEl = container.querySelector('#rect-event-count');
const lang = getLang();
countEl.textContent = `${rectEvents.length} ${t('rect.event.count')}`;
if (!rectEvents.length) { listEl.innerHTML = `<p class="rect-empty">${t('rect.no.events')}</p>`; return; }
listEl.innerHTML = rectEvents.map((evt, i) => {
const cat = EVENT_CATEGORIES[evt.category];
return `<div class="rect-event-item">
<span class="rect-evt-icon">${cat.icon}</span>
<span class="rect-evt-date">${escapeHtml(evt.date)}</span>
<span class="rect-evt-cat">${escapeHtml(lang === 'en' ? cat.en : cat.cn)} (${escapeHtml(cat.varga)})</span>
<span class="rect-evt-desc">${escapeHtml(evt.desc)}</span>
<button class="rect-evt-del" data-idx="${escapeAttr(i)}">✕</button></div>`;
}).join('');
listEl.querySelectorAll('.rect-evt-del').forEach(b => {
b.addEventListener('click', () => { rectEvents.splice(+b.dataset.idx, 1); renderEventList(container); });
});
}
async function handleRun(container, runBtn) {
if (!rectEvents.length) { alert(t('rect.alert.no.events')); return; }
if (!window.__jyotishBirth) { alert(t('rect.alert.no.chart')); return; }
const q = s => container.querySelector(s);
const progressEl = q('#rect-progress'), fillEl = q('#rect-progress-fill');
const textEl = q('#rect-progress-text'), resultsEl = q('#rect-results');
const rangeMin = +q('#rect-range').value, stepMin = +q('#rect-step').value;
progressEl.classList.remove('hidden'); resultsEl.classList.add('hidden');
runBtn.querySelector('.btn-text').classList.add('hidden');
runBtn.querySelector('.btn-loading').classList.remove('hidden'); runBtn.disabled = true;
try {
const result = await runRectification(window.__jyotishBirth, rectEvents, {
rangeMin, stepMin,
onProgress(cur, total) {
const pct = Math.round(cur / total * 100);
fillEl.style.width = pct + '%';
textEl.textContent = `${t('rect.calculating')} ${cur}/${total} (${pct}%)`;
},
});
if (result.error) { textEl.textContent = result.error; return; }
renderResults(container, result);
} catch (e) { textEl.textContent = `Error: ${e.message}`; console.error('[Rect]', e);
} finally {
runBtn.querySelector('.btn-text').classList.remove('hidden');
runBtn.querySelector('.btn-loading').classList.add('hidden');
runBtn.disabled = false;
setTimeout(() => progressEl.classList.add('hidden'), 2000);
}
}
function renderResults(container, result) {
const el = container.querySelector('#rect-results'); el.classList.remove('hidden');
const lang = getLang();
const { bestMatch: bm, confidence: conf, baseChartInfo: base, results: all } = result;
const audit = result.audit || {};
const decisionPlan = result.decisionPlan || {};
const confClr = { '高': '#22c55e', '中': '#f59e0b', '低': '#ef4444', '不确定': '#9ca3af' };
const correctedBirth = buildCorrectedBirth(result.birth, bm.offsetMin);
const reportText = buildRectificationReportText(result, correctedBirth);
// 分盘变化
const vcHtml = bm.vargaChanges.length > 0
? bm.vargaChanges.map(c => `<tr><td>${c.varga}</td><td>${signName(c.from)}</td><td>${signName(c.to)}</td></tr>`).join('')
: `<tr><td colspan="3" style="text-align:center">${t('rect.no.change')}</td></tr>`;
// 评分条
const weights = { dasha:'40%', varga:'35%', house:'15%', nak:'10%' };
const scoreBars = Object.keys(weights).map(k => {
const s = bm.scores[k];
return `<div class="rect-score-row">
<span class="rect-score-label">${t('rect.scoring.'+k)} (${weights[k]})</span>
<div class="rect-score-bar"><div class="rect-score-fill" style="width:${pctStyle(s.pct)}"></div></div>
<span class="rect-score-val">${escapeHtml(s.pct)}%</span></div>`;
}).join('');
const auditCards = renderAuditCards(audit, conf, lang);
const warningHtml = (audit.warnings || []).length
? `<div class="rect-audit-warnings">${audit.warnings.map(w => `<p>${escapeHtml(w)}</p>`).join('')}</div>`
: '';
// 事件详情
const evtRows = bm.eventScores.map(es => {
const cat = EVENT_CATEGORIES[es.event.category];
const rel = getRelevanceText(es.dasha, es.event.category, bm.ascSign);
return `<tr>
<td>${cat.icon} ${escapeHtml(lang==='en'?cat.en:cat.cn)} <small>(${escapeHtml(cat.varga)})</small></td>
<td>${escapeHtml(es.event.date)}</td>
<td>${escapeHtml(es.dasha ? planetName(es.dasha.mahadasha) : '—')}</td>
<td>${escapeHtml(es.dasha?.antardasha ? planetName(es.dasha.antardasha) : '—')}</td>
<td class="${es.dashaScore>0?'rect-score-pos':'rect-score-neg'}">${escapeHtml(es.dashaScore.toFixed(1))}</td>
<td class="${es.vargaScore>0?'rect-score-pos':'rect-score-neg'}">${escapeHtml(es.vargaScore.toFixed(1))}</td>
<td class="rect-relevance">${escapeHtml(rel)}</td></tr>`;
}).join('');
el.innerHTML = `
<div class="rect-result-summary card">
<h4 class="sub-title">${t('rect.result')}</h4>
<div class="rect-summary-grid">
<div class="rect-summary-item"><span class="rect-label">${t('rect.original.time')}</span><span class="rect-value">${escapeHtml(base.time)}</span></div>
<div class="rect-summary-item"><span class="rect-label">${t('rect.rec.time')}</span><span class="rect-value rect-best">${escapeHtml(bm.time)} (${escapeHtml(fmtOffset(bm.offsetMin))})</span></div>
<div class="rect-summary-item"><span class="rect-label">${t('rect.confidence')}</span><span class="rect-value" style="color:${confClr[conf.level]||'#9ca3af'}">${conf.level} (${conf.bestPct}%)</span></div>
</div>
<div class="rect-recommendations">${conf.recommendation.map(r => `<p class="rect-rec-line">${escapeHtml(r)}</p>`).join('')}</div>
${warningHtml}
<div class="rect-result-actions">
<button type="button" class="rect-secondary-btn" id="rect-copy-report">复制校正摘要</button>
<button type="button" class="rect-apply-btn" id="rect-apply-time">应用推荐时间重新排盘</button>
</div>
</div>
<div class="rect-audit card">
<h4 class="sub-title">证据闭环</h4>
<div class="rect-audit-grid">${auditCards}</div>
</div>
<div class="rect-plan card">
<h4 class="sub-title">分盘调用顺序</h4>
<p class="rect-plan-principle">${escapeHtml(decisionPlan.principle || 'Dasha 定框,核心分盘先行,专项分盘后置。')}</p>
<ol class="rect-plan-list">${(decisionPlan.ordered_layers || []).map(layer => `
<li>
<div class="rect-plan-row">
<strong>${escapeHtml(layer.label)}</strong>
<span>${escapeHtml(layer.role)}</span>
</div>
<p>${escapeHtml(layer.reason)}</p>
</li>
`).join('')}</ol>
${(decisionPlan.selected_theme_vargas || []).length ? `<p class="rect-plan-focus">本轮优先专项分盘:${escapeHtml(decisionPlan.selected_theme_vargas.join(' / '))}</p>` : ''}
${(decisionPlan.warnings || []).length ? `<div class="rect-audit-warnings">${decisionPlan.warnings.map(w => `<p>${escapeHtml(w)}</p>`).join('')}</div>` : ''}
</div>
<div class="rect-scoring-detail card">
<h4 class="sub-title">${t('rect.scoring.title')}</h4>
${scoreBars}
<div class="rect-score-total">${t('rect.total.score')} ${bm.totalScore}%</div>
</div>
<div class="rect-varga-changes card">
<h4 class="sub-title">${t('rect.varga.changes')}</h4>
<table class="rect-table"><thead><tr><th>${t('rect.varga.col')}</th><th>${t('rect.from.col')}</th><th>${t('rect.to.col')}</th></tr></thead>
<tbody>${vcHtml}</tbody></table>
</div>
<div class="rect-top-results card">
<h4 class="sub-title">${t('rect.top.candidates')}</h4>
<div class="rect-table-wrap"><table class="rect-table">
<thead><tr><th>#</th><th>${t('rect.time')}</th><th>${t('rect.offset')}</th><th>${t('rect.asc')}</th><th>D9</th><th>D10</th><th>${t('rect.score')}</th><th>${t('rect.match')}</th></tr></thead>
<tbody>${all.slice(0,10).map((r,i) => {
const d9=r.vargaLagnas?.D9?.sign||r.ascSign, d10=r.vargaLagnas?.D10?.sign||r.ascSign;
const d9chg=r.vargaChanges?.some(v=>v.varga==='D9')?' ⚠️':'';
const d10chg=r.vargaChanges?.some(v=>v.varga==='D10')?' ⚠️':'';
return `<tr class="${r.isBest?'rect-best-row':''}" data-offset="${escapeAttr(r.offsetMin)}">
<td>${i+1}</td><td>${escapeHtml(r.time)}</td><td>${escapeHtml(fmtOffset(r.offsetMin))}</td>
<td>${escapeHtml(signName(r.ascSign))}</td><td>${escapeHtml(signName(d9))}${d9chg}</td><td>${escapeHtml(signName(d10))}${d10chg}</td>
<td>${escapeHtml(r.totalScore)}%</td>
<td><div class="rect-match-bar"><div class="rect-match-fill" style="width:${pctStyle(r.totalScore)}"></div><span class="rect-match-pct">${escapeHtml(r.totalScore)}%</span></div></td></tr>`;
}).join('')}</tbody>
</table></div>
</div>
<div class="rect-event-detail card">
<h4 class="sub-title">${t('rect.event.detail')}</h4>
<div class="rect-table-wrap"><table class="rect-table rect-detail-table">
<thead><tr><th>${t('rect.event.col')}</th><th>${t('rect.date.col')}</th><th>${t('rect.maha.col')}</th><th>${t('rect.antar.col')}</th><th>${t('rect.score.dasha')}</th><th>${t('rect.score.varga')}</th><th>${t('rect.rel.col')}</th></tr></thead>
<tbody>${evtRows}</tbody>
</table></div>
</div>`;
el.querySelector('#rect-apply-time')?.addEventListener('click', () => {
document.dispatchEvent(new CustomEvent('jyotish:apply-rectified-birth', {
detail: { birth: correctedBirth, rectification: result },
}));
});
el.querySelector('#rect-copy-report')?.addEventListener('click', async () => {
await copyRectificationReport(reportText);
const btn = el.querySelector('#rect-copy-report');
if (btn) {
const oldText = btn.textContent;
btn.textContent = '已复制';
setTimeout(() => { btn.textContent = oldText; }, 1200);
}
});
el.querySelectorAll('.rect-top-results tr[data-offset]').forEach(tr => {
tr.style.cursor = 'pointer';
tr.addEventListener('click', () => {
const off = parseFloat(tr.dataset.offset);
const r = all.find(x => x.offsetMin === off);
if (r) showOffsetDetail(el, r, base);
});
});
}
function buildCorrectedBirth(birth, offsetMin) {
const date = new Date(birth.year, birth.month - 1, birth.day, birth.hour, birth.minute || 0, 0);
date.setMinutes(date.getMinutes() + offsetMin);
return {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
hour: date.getHours(),
minute: date.getMinutes(),
lat: birth.lat,
lon: birth.lon,
tz: birth.tz,
};
}
function buildRectificationReportText(result, correctedBirth) {
const bm = result.bestMatch || {};
const conf = result.confidence || {};
const audit = result.audit || {};
const lines = [
'Janma Samaya Shuddhi 生时校正摘要',
`原始时间:${result.baseChartInfo?.time || '-'}`,
`推荐时间:${fmtTime(correctedBirth.hour, correctedBirth.minute)} (${fmtOffset(bm.offsetMin || 0)})`,
`推荐日期:${correctedBirth.year}-${String(correctedBirth.month).padStart(2, '0')}-${String(correctedBirth.day).padStart(2, '0')}`,
`置信度:${conf.level || '不确定'} / ${conf.bestPct || 0}%`,
`事件覆盖:${audit.coverage?.event_count || 0} 个事件,${audit.coverage?.group_count || 0} 类主题,跨度 ${audit.coverage?.year_span || 0}`,
`候选差距:领先 ${audit.score_gap ?? 0} 分;同分簇 ${audit.top_cluster?.count || 0}`,
`使用边界:${conf.recommendation?.join('') || '建议补充事件后复核。'}`,
];
if (audit.warnings?.length) {
lines.push(`警告:${audit.warnings.join('')}`);
}
return lines.join('\n');
}
async function copyRectificationReport(text) {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
}
function renderAuditCards(audit, conf, lang) {
const coverage = audit.coverage || {};
const evidence = audit.evidence || {};
const cluster = audit.top_cluster || {};
const missing = coverage.missing_groups?.length ? coverage.missing_groups.join('、') : '暂不需要';
const runner = audit.runner_up ? `${audit.runner_up.time} (${fmtOffset(audit.runner_up.offsetMin)}, ${audit.runner_up.totalScore}%)` : '无';
const confidenceMeaning = {
'高': '可把推荐时间作为主候选,但仍建议保留原始记录。',
'中': '可用于D1/D9/D10观察,高敏感分盘需谨慎。',
'低': '只能作为探索候选,不建议覆盖出生证明/家人记录。',
'不确定': '证据不足,应继续收集事件。',
}[conf?.level] || '证据不足,应继续收集事件。';
const cards = [
['事件覆盖', `${coverage.event_count || 0}个事件 · ${coverage.group_count || 0}类主题`, `质量 ${coverage.quality_score || 0}%;年份跨度 ${coverage.year_span || 0} 年。`],
['命中证据', `${evidence.matched_events || 0}个匹配 · ${evidence.match_rate || 0}%`, `强证据 ${evidence.strong_events || 0} 个;敏感分盘 ${evidence.sensitive_vargas?.join('/') || '无变化'}`],
['候选差距', `领先 ${audit.score_gap ?? 0}`, `第二名:${runner};同分簇 ${cluster.count || 0} 个,范围 ${fmtOffset(cluster.min || 0)}${fmtOffset(cluster.max || 0)}`],
['使用边界', conf?.level || '不确定', confidenceMeaning],
['补充建议', missing, '优先补充不同年龄段、不同主题、日期明确的事件。'],
];
return cards.map(([label, value, note]) => `
<div class="rect-audit-card">
<span>${escapeHtml(label)}</span>
<strong>${escapeHtml(value)}</strong>
<p>${escapeHtml(note)}</p>
</div>
`).join('');
}
function getRelevanceText(di, category, ascSign) {
if (!di) return '—';
const cat = EVENT_CATEGORIES[category]; if (!cat) return '—';
const ai = SIGNS.indexOf(ascSign), hl = cat.houses.map(h => getHouseLord(ai, h));
const mdR = cat.planets.includes(di.mahadasha) || hl.includes(di.mahadasha);
const adR = di.antardasha && (cat.planets.includes(di.antardasha) || hl.includes(di.antardasha));
if (mdR && adR) return t('rect.strong.match');
if (mdR) return t('rect.maha.match');
if (adR) return t('rect.antar.match');
return t('rect.no.match');
}
function showOffsetDetail(container, r, base) {
let det = container.querySelector('.rect-offset-detail');
if (!det) { det = document.createElement('div'); det.className = 'rect-offset-detail card'; container.appendChild(det); }
const vlHtml = Object.entries(r.vargaLagnas || {}).map(([k, v]) =>
`<span class="rect-change-tag">${escapeHtml(k)}: ${escapeHtml(v ? signName(v.sign) : '—')}</span>`
).join('');
const hcHtml = r.houseChanges.length > 0
? r.houseChanges.map(c => `<span class="rect-change-tag">${escapeHtml(planetName(c.planet))}: H${escapeHtml(c.from)}→H${escapeHtml(c.to)}</span>`).join('')
: escapeHtml(t('rect.no.change'));
det.innerHTML = `
<h4 class="sub-title">${escapeHtml(t('rect.offset.detail').replace('{0}',fmtOffset(r.offsetMin)).replace('{1}',r.time))}</h4>
<div class="rect-detail-grid">
<div><strong>${t('rect.asc.label')}</strong>${escapeHtml(signName(r.ascSign))} ${escapeHtml(r.ascDeg?.toFixed(2) || '-')}°</div>
<div><strong>${t('rect.moon.nak')}</strong>${escapeHtml(r.moonNak?.nakName||'—')} Pada ${escapeHtml(r.moonNak?.pada||'?')}</div>
<div><strong>${t('rect.total.score')}</strong>${escapeHtml(r.totalScore)}%</div>
</div>
<h5 class="rect-sub">${t('rect.varga.changes')}</h5>
<div class="rect-changes">${vlHtml}</div>
<h5 class="rect-sub">${t('rect.house.changes')}</h5>
<div class="rect-changes">${hcHtml}</div>`;
det.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
export function initRectification() { rectEvents = []; rectInterviewAnswers = {}; }
export function setRectificationRecommendedEvents(recommendedEvents = []) {
rectRecommendedEvents = Array.isArray(recommendedEvents) ? recommendedEvents : [];
}
function renderRectificationInterview(lang) {
const recommended = buildRecommendedRectificationQuestions(rectRecommendedEvents);
const fallback = recommended.length ? [] : buildRectificationInterviewQuestions();
return [...recommended, ...fallback].slice(0, recommended.length ? recommended.length : 3)
.map(question => renderInterviewQuestion(question, lang))
.join('');
}