Личное пространство астрологических разборов

Ведическая астрология, с которой становится яснее

Личные разборы по натальной карте, отношениям, текущему периоду и вопросам момента — красиво, глубоко и по делу.

Здесь не общий гороскоп ради эффекта, а персональная работа с вашей картой, периодом и вопросом. Мягкий вход, ясная логика и ощущение частного премиального пространства.

Форматы, с которых можно начать

Мягкий вход

Авто

Помогает спокойно зайти в систему и понять, какой формат разбора подойдет именно сейчас.

Базовая карта

Карта рождения

Личная карта характера, задач, сильных сторон и внутренних опор без потока общих формулировок.

Для ребёнка

Детская астрология

Мягкий разбор темперамента, талантов, ритма обучения и поддержки ребёнка без жёстких прогнозов.

Отношения

Совместимость

Динамика пары, притяжение, сложные зоны и потенциал отношений в одном живом разборе.

Текущий период

Транзиты

Показывают, что активировано сейчас и как проживать период точнее, спокойнее и с большей опорой.

Вопрос момента

Прашна

Точный ответ на конкретный вопрос по моменту: да, нет, с задержкой или при условии.

Как это работает

С чем это помогает

  • лучше понять себя и свои сильные стороны
  • разобраться в отношениях и внутренних узлах пары
  • увидеть смысл текущего периода и его нагрузку
  • прояснить решение в конкретном вопросе

Три спокойных шага

1. Вы выбираете формат разбора под свой вопрос или период.
2. Вводите данные и, если нужно, сам вопрос.
3. Получаете разбор и можете продолжить его дальше уже внутри своего клиентского пространства.

Ваш профиль

После первого успешного разбора профиль создаётся автоматически. Здесь можно посмотреть токены, ссылку и состояние профиля.

Начать разбор

Ваш профиль

Свой вопрос
Основная карта
Можно загрузить JSON/TXT-файл с полями `birth_date`, `birth_time`, `birth_place` или полный экспорт карты с `user`, `houses`, `bhava`, `graha`.
Начните вводить город и выберите вариант из списка. Для маленького поселка укажите ближайший город.

1 токен = 1 рубль

Загружаю прайс...
Карта момента вопроса
Для совместимости показываются две карты рядом, для остальных сценариев — одна карта текущего разбора.

Основная карта

Карта, на которой строится текущий разбор

Итог разбора

тип вопроса: - сценарий: - промпт: -
Здесь появится ответ на Ваш вопрос.
Панель отладки
Краткая сводка debug ещё не загружена.
Отладочные данные ещё не загружены.

История диалога

История начнёт собираться после первого ответа. Здесь удобно проверять follow-up, handoff и redirect.

`; } function buildReportPayload(result) { const title = reportModeTitles[state.mode] || "Астрологический разбор"; return { title, question: String(result?.question || getCurrentQuestionText() || "").trim(), answer: String(result?.answer || "").trim(), site_url: window.location.origin || "http://127.0.0.1:8000", generated_at: new Date().toLocaleString("ru-RU"), cards: getReportCards().map((card) => ({ title: card.title, rows: card.rows.map(([label, value]) => ({ label, value: String(value || "").trim() || "-", })), })), }; } function reportFileName() { const modeTitle = (reportModeTitles[state.mode] || "astro-report") .toLowerCase() .replace(/\s+/g, "-") .replace(/[^a-zа-яё0-9-]/gi, ""); const datePart = new Date().toISOString().slice(0, 10); return `${modeTitle || "astro-report"}-${datePart}.pdf`; } async function downloadCurrentPdfReport() { const result = state.lastWorkspaceResult; if (!result?.answer) { setStatus("Сначала получите ответ, затем можно скачать отчет.", "error"); return; } const originalText = downloadPdfReportButtonEl?.textContent || "Скачать PDF"; if (downloadPdfReportButtonEl) { downloadPdfReportButtonEl.disabled = true; downloadPdfReportButtonEl.textContent = "Готовлю PDF..."; } try { const response = await fetch("/report/pdf", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(buildReportPayload(result)), }); if (!response.ok) { const message = await response.text(); throw new Error(message || "PDF не удалось сформировать."); } const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = reportFileName(); document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); setStatus("PDF-отчет скачан."); } catch (error) { setStatus(error instanceof Error ? error.message : "Не удалось скачать PDF.", "error"); } finally { if (downloadPdfReportButtonEl) { downloadPdfReportButtonEl.disabled = false; downloadPdfReportButtonEl.textContent = originalText; } } } function renderAnswer(result) { const answer = result?.answer?.trim(); if (window.JyotishTerms && answerBoxEl) { window.JyotishTerms.setElementText(answerBoxEl, answer || "Система не вернула текстовый ответ."); } else { answerBoxEl.textContent = answer || "Система не вернула текстовый ответ."; } downloadPdfReportButtonEl?.classList.toggle("hidden", !answer); setCompatibilityBetweenLines(result?.between_lines || ""); renderAutoNextStep(questionEl?.value || result?.question || ""); if (state.mode === "prashna" && answer && !state.clientProfile) { state.prashnaProfileReveal = true; updateModeUi(); } } const SpeechRecognitionCtor = window.SpeechRecognition || window.webkitSpeechRecognition; let questionSpeechRecognition = null; let questionSpeechRecording = false; let questionSpeechBaseText = ""; function syncVoiceQuestionButton() { if (!voiceQuestionButtonEl) { return; } if (window.PaidVoiceInput) { voiceQuestionButtonEl.classList.remove("hidden"); voiceQuestionButtonEl.classList.toggle("is-recording", false); voiceQuestionButtonEl.setAttribute("aria-label", "Голосовой ввод"); voiceQuestionButtonEl.title = "Голосовой ввод"; return; } voiceQuestionButtonEl.classList.toggle("hidden", !SpeechRecognitionCtor); voiceQuestionButtonEl.classList.toggle("is-recording", questionSpeechRecording); voiceQuestionButtonEl.setAttribute( "aria-label", questionSpeechRecording ? "Остановить голосовой ввод" : "Голосовой ввод вопроса", ); voiceQuestionButtonEl.title = questionSpeechRecording ? "Остановить голосовой ввод" : "Голосовой ввод"; } function buildQuestionSpeechText(finalText, interimText = "") { const parts = [questionSpeechBaseText, finalText, interimText] .map((part) => String(part || "").trim()) .filter(Boolean); return parts.join(" "); } function dispatchQuestionInput() { questionEl?.dispatchEvent(new Event("input", { bubbles: true })); } function stopQuestionSpeechRecognition() { if (!questionSpeechRecognition || !questionSpeechRecording) { return; } questionSpeechRecording = false; syncVoiceQuestionButton(); questionSpeechRecognition.stop(); } function startQuestionSpeechRecognition() { if (!SpeechRecognitionCtor || !questionEl) { setStatus("Голосовой ввод не поддерживается этим браузером.", true); return; } questionSpeechRecognition = new SpeechRecognitionCtor(); questionSpeechBaseText = questionEl.value.trim(); let finalTranscript = ""; questionSpeechRecognition.lang = "ru-RU"; questionSpeechRecognition.interimResults = true; questionSpeechRecognition.continuous = true; questionSpeechRecognition.maxAlternatives = 1; questionSpeechRecognition.onstart = () => { questionSpeechRecording = true; syncVoiceQuestionButton(); setStatus("Слушаю вопрос..."); }; questionSpeechRecognition.onresult = (event) => { let interimTranscript = ""; finalTranscript = ""; for (let index = 0; index < event.results.length; index += 1) { const transcript = event.results[index][0]?.transcript || ""; if (event.results[index].isFinal) { finalTranscript += ` ${transcript}`; } else { interimTranscript += ` ${transcript}`; } } questionEl.value = buildQuestionSpeechText(finalTranscript, interimTranscript); dispatchQuestionInput(); }; questionSpeechRecognition.onerror = (event) => { questionSpeechRecording = false; syncVoiceQuestionButton(); const error = event?.error || "unknown"; const errorMessages = { "not-allowed": "Браузер не дал доступ к микрофону. Проверьте разрешение для сайта.", "service-not-allowed": "Браузер заблокировал сервис распознавания речи.", "audio-capture": "Браузер не видит микрофон. Проверьте устройство ввода.", "no-speech": "Я не услышала речь. Нажмите микрофон и начните говорить сразу после включения.", "network": "Сервис распознавания речи Edge сейчас недоступен. Попробуйте Chrome или повторите позже.", "aborted": "Голосовой ввод остановлен.", }; const message = errorMessages[error] || `Не удалось распознать речь (${error}). Попробуйте ещё раз.`; setStatus(message, true); }; questionSpeechRecognition.onnomatch = () => { setStatus("Речь услышана, но браузер не смог разобрать слова. Попробуйте сказать вопрос чуть медленнее.", true); }; questionSpeechRecognition.onend = () => { questionSpeechRecording = false; syncVoiceQuestionButton(); if (questionEl.value.trim()) { dispatchQuestionInput(); } }; try { questionSpeechRecognition.start(); } catch { questionSpeechRecording = false; syncVoiceQuestionButton(); } } function renderDebug(result) { const summary = { configured_model: result?.configured_model ?? null, actual_model: result?.actual_model ?? null, llm_models: result?.llm_models ?? null, llm_timings_ms: result?.llm_timings_ms ?? null, timing_ms: result?.timing_ms ?? null, }; debugSummaryBoxEl.textContent = JSON.stringify(summary, null, 2); debugBoxEl.textContent = JSON.stringify(result, null, 2); } function renderHistory() { historyListEl.innerHTML = ""; historyEmptyEl.classList.toggle("hidden", state.history.length > 0); for (const item of [...state.history].reverse()) { const article = document.createElement("article"); article.className = "history-item"; article.innerHTML = `

${item.modeLabel}

${item.questionType} ${item.scenarioName} ${item.promptName}

${item.question}

${item.answerPreview}

`; historyListEl.appendChild(article); } } function updateChartMeta(target, payload, subtitle) { const lines = [`стиль: ${payload?.chart_style ?? "-"}`]; target.innerHTML = lines.map((line) => `
${line}
`).join(""); } function applyCardData(fields, cardData) { fields.birth_date.value = cardData.birth_date ?? cardData.date ?? ""; fields.birth_time.value = cardData.birth_time ?? cardData.time ?? ""; if (fields.birth_time_quality) { fields.birth_time_quality.value = normalizeBirthTimeQuality(cardData.birth_time_quality ?? cardData.time_quality ?? "exact"); } const birthPlace = cardData.birth_place ?? cardData.place ?? ""; fields.birth_place.value = birthPlace; const birthPlaceTarget = fields === primaryFields ? "primary" : fields === personAFields ? "person_a" : fields === personBFields ? "person_b" : ""; if (birthPlaceTarget) { markBirthPlaceSelected(birthPlaceTarget, birthPlace); if (birthPlace) { setBirthPlaceNote(birthPlaceTarget, `Выбрано: ${birthPlace}`); } } } function unwrapStructuredPayload(data) { if (!data || typeof data !== "object") { return data; } const nestedCandidates = [ data.provider_payload, data.raw_chart, data.chart_payload, data.chart_data, data.payload, data.data, ]; for (const candidate of nestedCandidates) { if (candidate && typeof candidate === "object") { return candidate; } } return data; } function formatCoordsPlace(userData) { if (!userData || typeof userData !== "object") { return ""; } const latitude = userData.latitude; const longitude = userData.longitude; const timezone = userData.timezone; if (latitude === undefined || longitude === undefined) { return ""; } const coords = `Координаты: ${latitude}, ${longitude}`; return timezone ? `${coords} (${timezone})` : coords; } function extractBirthFromUserBlock(userData) { if (!userData || typeof userData !== "object") { return null; } const datetime = String(userData.datetime ?? "").trim(); if (!datetime) { return null; } const [birthDate = "", birthTimeRaw = ""] = datetime.split(" "); const birthTime = birthTimeRaw.slice(0, 5); return { birth_date: birthDate, birth_time: birthTime, birth_place: userData.birth_place ?? userData.place ?? userData.place_label ?? userData.location ?? formatCoordsPlace(userData), }; } function extractSingleCard(data) { const normalizedData = unwrapStructuredPayload(data); if (!normalizedData || typeof normalizedData !== "object") { return null; } const birthInfo = normalizedData.birth_info; if (birthInfo && typeof birthInfo === "object") { const extractedBirthInfo = { birth_date: birthInfo.birth_date ?? "", birth_time: birthInfo.birth_time ?? "", birth_place: birthInfo.birth_place ?? "", }; if (extractedBirthInfo.birth_date || extractedBirthInfo.birth_time || extractedBirthInfo.birth_place) { return extractedBirthInfo; } } const directBirthData = normalizedData.birth_date || normalizedData.birth_time || normalizedData.birth_place; if (directBirthData) { return normalizedData; } const userBirthData = extractBirthFromUserBlock(normalizedData.user); if (userBirthData) { return userBirthData; } const requestContextBirthData = normalizedData.request_context ? { birth_date: normalizedData.request_context.birth_date, birth_time: normalizedData.request_context.birth_time, birth_place: normalizedData.request_context.birth_place, } : null; if (requestContextBirthData?.birth_date || requestContextBirthData?.birth_time || requestContextBirthData?.birth_place) { return requestContextBirthData; } const nestedCandidates = [ normalizedData.normalized_raw_context, normalizedData.base, normalizedData.base?.normalized_raw_context, normalizedData.natal_context, normalizedData.compatibility_context, normalizedData.person_A, normalizedData.person_B, normalizedData.person_a, normalizedData.person_b, normalizedData.birth_data, normalizedData.card, normalizedData.person, normalizedData.profile, normalizedData.subject, normalizedData.owner, ]; for (const candidate of nestedCandidates) { if (candidate && typeof candidate === "object") { const extracted = extractSingleCard(candidate); if (extracted) { return extracted; } } } return null; } function buildChartPayloadFromRawExport(data) { const normalizedData = unwrapStructuredPayload(data); if (!normalizedData || typeof normalizedData !== "object") { return null; } const houses = normalizedData.houses; const bhava = normalizedData.bhava; const lagnaSignNumber = Number(normalizedData?.lagna?.Lg?.rashi ?? 0); if (!houses || typeof houses !== "object") { return null; } const normalizedHouses = []; for (let houseNumber = 1; houseNumber <= 12; houseNumber += 1) { const houseKey = String(houseNumber); const houseData = houses[houseKey]; const bhavaData = bhava && typeof bhava === "object" ? bhava[houseKey] : null; const graha = houseData.graha; const natalPlanets = Array.isArray(graha) ? [] : Object.keys(graha ?? {}); let signNumber = bhavaData?.rashi ?? ""; if (!signNumber && lagnaSignNumber) { signNumber = ((lagnaSignNumber - 1 + (houseNumber - 1)) % 12) + 1; } normalizedHouses.push({ house_number: houseNumber, sign_number: signNumber, natal_planets: natalPlanets, transit_planets: [], is_asc: houseNumber === 1, }); } return { chart_style: "north_indian", asc_house_number: 1, has_transits: false, houses: normalizedHouses, }; } function buildChartPayloadFromNormalizedContext(context) { if (!context || typeof context !== "object" || !Array.isArray(context.houses)) { return null; } const natalPlanetsByHouse = {}; Object.values(context.planets || {}).forEach((planet) => { const houseNumber = Number(planet?.house_number || 0); const code = String(planet?.code || "").trim(); if (!houseNumber || !code) { return; } if (!natalPlanetsByHouse[houseNumber]) { natalPlanetsByHouse[houseNumber] = []; } natalPlanetsByHouse[houseNumber].push({ code, degree: planet?.degree_in_sign ?? null, retrograde: Boolean(planet?.retrograde), }); }); const houses = context.houses .slice() .sort((left, right) => Number(left?.house_number || 0) - Number(right?.house_number || 0)) .map((house) => { const houseNumber = Number(house?.house_number || 0); return { house_number: houseNumber, sign_number: house?.sign_number ?? null, sign_name: house?.sign_name ?? null, natal_planets: natalPlanetsByHouse[houseNumber] || [], transit_planets: [], aspect_planets: [], is_asc: houseNumber === 1, }; }); if (houses.length !== 12) { return null; } return { chart_style: "north_indian", chart_format: "house_based", asc_house_number: 1, has_transits: false, houses, }; } function buildTransitOverlayChartPayload(natalData, transitData) { const natalPayload = buildChartPayloadFromRawExport(natalData); const normalizedNatal = unwrapStructuredPayload(natalData); const normalizedTransit = unwrapStructuredPayload(transitData); const natalLagnaSignNumber = Number(normalizedNatal?.lagna?.Lg?.rashi ?? 0); const transitGraha = normalizedTransit?.graha; if ( !natalPayload || !natalLagnaSignNumber || !transitGraha || typeof transitGraha !== "object" ) { return natalPayload; } const transitPlanetsByNatalHouse = {}; for (let houseNumber = 1; houseNumber <= 12; houseNumber += 1) { transitPlanetsByNatalHouse[houseNumber] = []; } Object.keys(transitGraha).forEach((planetCode) => { const signNumber = Number(transitGraha?.[planetCode]?.rashi ?? 0); if (!signNumber) { return; } const natalHouseNumber = ((signNumber - natalLagnaSignNumber + 12) % 12) + 1; transitPlanetsByNatalHouse[natalHouseNumber].push(planetCode); }); return { ...natalPayload, has_transits: true, houses: (natalPayload.houses ?? []).map((house) => ({ ...house, transit_planets: transitPlanetsByNatalHouse[house.house_number] ?? [], })), }; } function extractPairCards(data) { const normalizedData = unwrapStructuredPayload(data); if (!normalizedData || typeof normalizedData !== "object") { return null; } const compatibilityContext = normalizedData.compatibility_context && typeof normalizedData.compatibility_context === "object" ? unwrapStructuredPayload(normalizedData.compatibility_context) : null; const personASource = normalizedData.person_a ?? normalizedData.person_A ?? compatibilityContext?.person_a ?? compatibilityContext?.person_A ?? normalizedData.man ?? normalizedData.male ?? normalizedData.men ?? null; const personBSource = normalizedData.person_b ?? normalizedData.person_B ?? compatibilityContext?.person_b ?? compatibilityContext?.person_B ?? normalizedData.woman ?? normalizedData.female ?? normalizedData.women ?? null; if (personASource && personBSource) { return { person_a: extractSingleCard(personASource), person_b: extractSingleCard(personBSource), raw_person_a: unwrapStructuredPayload(personASource), raw_person_b: unwrapStructuredPayload(personBSource), }; } if (normalizedData.person_a && normalizedData.person_b) { return { person_a: extractSingleCard(normalizedData.person_a), person_b: extractSingleCard(normalizedData.person_b), raw_person_a: unwrapStructuredPayload(normalizedData.person_a), raw_person_b: unwrapStructuredPayload(normalizedData.person_b), }; } if (Array.isArray(normalizedData.people) && normalizedData.people.length >= 2) { return { person_a: extractSingleCard(normalizedData.people[0]), person_b: extractSingleCard(normalizedData.people[1]), raw_person_a: unwrapStructuredPayload(normalizedData.people[0]), raw_person_b: unwrapStructuredPayload(normalizedData.people[1]), }; } const personA = extractSingleCard({ birth_date: normalizedData.person_a_birth_date, birth_time: normalizedData.person_a_birth_time, birth_place: normalizedData.person_a_birth_place, }); const personB = extractSingleCard({ birth_date: normalizedData.person_b_birth_date, birth_time: normalizedData.person_b_birth_time, birth_place: normalizedData.person_b_birth_place, }); if (personA?.birth_date || personA?.birth_time || personA?.birth_place || personB?.birth_date || personB?.birth_time || personB?.birth_place) { return { person_a: personA, person_b: personB, raw_person_a: normalizedData, raw_person_b: normalizedData, }; } return null; } function sanitizeStructuredText(text) { return String(text ?? "") .replace(/^\uFEFF/, "") .replace(/^\s*```(?:json)?/i, "") .replace(/```\s*$/i, "") .trim(); } function normalizeRelaxedJsonText(jsonText) { return String(jsonText ?? "") .replace( /(:\s*\{)[ \t]*[^"{[\]\r\n][^\r\n]*?(?=\r?\n[ \t]*")/g, "$1" ) .replace(/([{,]\s*)([A-Za-zА-Яа-я_][\wА-Яа-я_-]*)(\s*:)/g, '$1"$2"$3') .replace(/:\s*'([^'\\]*(?:\\.[^'\\]*)*)'/g, ': "$1"') .replace(/\bNone\b/g, "null") .replace(/\bTrue\b/g, "true") .replace(/\bFalse\b/g, "false") .replace(/,\s*([}\]])/g, "$1"); } function extractFirstBalancedJsonObject(text) { const source = String(text ?? ""); const start = source.indexOf("{"); if (start < 0) { return ""; } let depth = 0; let inString = false; let escaped = false; for (let index = start; index < source.length; index += 1) { const char = source[index]; if (inString) { if (escaped) { escaped = false; } else if (char === "\\") { escaped = true; } else if (char === '"') { inString = false; } continue; } if (char === '"') { inString = true; continue; } if (char === "{") { depth += 1; continue; } if (char === "}") { depth -= 1; if (depth === 0) { return source.slice(start, index + 1); } } } return source.slice(start); } function parseStructuredText(text) { const sanitized = sanitizeStructuredText(text); if (!sanitized) { throw new Error("Файл пустой."); } try { return JSON.parse(sanitized); } catch { const normalizedText = normalizeRelaxedJsonText(sanitized); try { return JSON.parse(normalizedText); } catch { const jsonSlice = extractFirstBalancedJsonObject(normalizedText); if (jsonSlice) { try { return JSON.parse(jsonSlice); } catch (relaxedError) { const details = relaxedError instanceof Error ? relaxedError.message : "неизвестная ошибка"; throw new Error(`Файл похож на нестрогий JSON. ${details}`); } } } const firstBrace = sanitized.indexOf("{"); const lastBrace = sanitized.lastIndexOf("}"); if (firstBrace >= 0 && lastBrace > firstBrace) { const jsonSlice = normalizeRelaxedJsonText( sanitized.slice(firstBrace, lastBrace + 1) ); try { return JSON.parse(jsonSlice); } catch (relaxedError) { const details = relaxedError instanceof Error ? relaxedError.message : "неизвестная ошибка"; throw new Error(`Файл похож на нестрогий JSON. ${details}`); } } throw new Error("Файл не похож на JSON-экспорт карты."); } } async function readJsonFile(file) { const text = await file.text(); return parseStructuredText(text); } function updateUploadNote(noteEl, message, isError = false) { noteEl.textContent = message; noteEl.style.color = isError ? "var(--danger)" : "var(--muted)"; } function triggerFileDialog(inputEl) { inputEl.value = ""; inputEl.click(); } function bindSingleCardUpload(buttonEl, inputEl, fields, noteEl, label, chartTarget = null, rawChartKey = null) { buttonEl.addEventListener("click", () => triggerFileDialog(inputEl)); inputEl.addEventListener("change", async () => { const file = inputEl.files?.[0]; if (!file) { return; } try { const data = await readJsonFile(file); const cardData = fields ? extractSingleCard(data) : null; if (fields && !cardData) { throw new Error("Не удалось извлечь дату, время и место из файла карты."); } if (fields && cardData) { applyCardData(fields, cardData); if (fields === primaryFields) { applyPrimaryIdentity(cardData); } else if (fields === personAFields || fields === personBFields) { applyCompatibilityIdentity(fields, cardData); } } if (rawChartKey) { state.uploadedRawCharts[rawChartKey] = unwrapStructuredPayload(data); if (rawChartKey === "primary" || rawChartKey === "transit") { clearTransitDetailsCache(); } } if (rawChartKey === "transit") { const transitCardData = extractSingleCard(data); const normalizedTransitData = unwrapStructuredPayload(data); const transitUser = normalizedTransitData?.user; if (transitCardData) { transitFields.transit_date.value = transitCardData.birth_date ?? ""; transitFields.transit_time.value = transitCardData.birth_time ?? ""; if (transitCardData.birth_place) { transitFields.transit_place.value = transitCardData.birth_place; } } if (transitUser && typeof transitUser === "object") { if (transitUser.timezone) { transitFields.transit_timezone.value = String(transitUser.timezone); } if (transitUser.latitude !== undefined && transitUser.latitude !== null) { transitFields.transit_latitude.value = formatCoordinateValue(transitUser.latitude); } if (transitUser.longitude !== undefined && transitUser.longitude !== null) { transitFields.transit_longitude.value = formatCoordinateValue(transitUser.longitude); } } } clearWorkspaceCacheForClientChange("Контекст предыдущего разбора сброшен: загружена новая карта."); let localChartPayload = buildChartPayloadFromRawExport(data); let localChartTarget = chartTarget; let localChartSubtitle = `загружено из файла / ${label.toLowerCase()}`; if ( state.mode === "transits" && ((rawChartKey === "transit" && state.uploadedRawCharts.primary) || (rawChartKey === "primary" && state.uploadedRawCharts.transit)) ) { localChartPayload = buildTransitOverlayChartPayload( rawChartKey === "primary" ? data : state.uploadedRawCharts.primary, rawChartKey === "transit" ? data : state.uploadedRawCharts.transit, ); localChartTarget = "primary"; localChartSubtitle = "транзиты / натальная карта + наложение транзитов"; } if (localChartTarget && localChartPayload) { queueChart(localChartTarget, localChartPayload, localChartSubtitle); } if ( state.mode === "compatibility" && (rawChartKey === "person_a" || rawChartKey === "person_b") && state.uploadedRawCharts.person_a && state.uploadedRawCharts.person_b ) { const chartA = buildChartPayloadFromRawExport(state.uploadedRawCharts.person_a); const chartB = buildChartPayloadFromRawExport(state.uploadedRawCharts.person_b); if (chartA && chartB) { queueCompatibilityOverlayChart(chartA, chartB); } } if (!fields) { updateUploadNote(noteEl, `${label}: загружен файл «${file.name}».`); } else if (!cardData.birth_place) { updateUploadNote( noteEl, `${label}: файл «${file.name}» загружен. Место не найдено в явном виде, поэтому его при необходимости можно вписать вручную.`, ); } else { updateUploadNote(noteEl, `${label}: загружен файл «${file.name}».`); } setStatus(`${label} обновлена из файла.`); } catch (error) { const message = error instanceof Error ? error.message : "Не удалось прочитать файл."; updateUploadNote(noteEl, `${label}: ${message}`, true); setStatus(message, true); } }); } function bindPairUpload() { pairUploadButtonEl.addEventListener("click", async () => { if (!isCompatibilityRoute) { return; } try { setStatus("Строю карты пары..."); await requestCompatibilityCharts(); updateUploadNote(pairUploadNoteEl, "Карты пары построены."); setStatus("Карты пары построены."); } catch (error) { const message = error instanceof Error ? error.message : "Не удалось построить карты пары."; updateUploadNote(pairUploadNoteEl, message, true); setStatus(message, true); } }); pairUploadInputEl.addEventListener("change", async () => { const file = pairUploadInputEl.files?.[0]; if (!file) { return; } try { const data = await readJsonFile(file); const normalizedData = unwrapStructuredPayload(data); const pair = extractPairCards(data); if (!pair?.person_a || !pair?.person_b) { throw new Error("Не удалось найти в файле person_a и person_b или извлечь из них birth-данные."); } applyCardData(personAFields, pair.person_a); applyCardData(personBFields, pair.person_b); applyCompatibilityIdentity(personAFields, pair.person_a); applyCompatibilityIdentity(personBFields, pair.person_b); state.uploadedRawCharts.person_a = pair.raw_person_a ?? unwrapStructuredPayload(normalizedData?.person_a ?? normalizedData?.person_A ?? normalizedData?.people?.[0]); state.uploadedRawCharts.person_b = pair.raw_person_b ?? unwrapStructuredPayload(normalizedData?.person_b ?? normalizedData?.person_B ?? normalizedData?.people?.[1]); clearWorkspaceCacheForClientChange("Контекст предыдущего разбора сброшен: загружена новая пара."); const chartA = buildChartPayloadFromRawExport(pair.raw_person_a ?? normalizedData?.person_a ?? normalizedData?.person_A ?? normalizedData?.people?.[0]); const chartB = buildChartPayloadFromRawExport(pair.raw_person_b ?? normalizedData?.person_b ?? normalizedData?.person_B ?? normalizedData?.people?.[1]); if (chartA) { queueChart("primary", chartA, "загружено из файла / карта мужчины"); } if (chartB) { queueChart("secondary", chartB, "загружено из файла / карта женщины"); } updateUploadNote(pairUploadNoteEl, `Пара загружена из файла «${file.name}».`); setStatus("Данные пары загружены из файла."); } catch (error) { const message = error instanceof Error ? error.message : "Не удалось прочитать файл пары."; updateUploadNote(pairUploadNoteEl, message, true); setStatus(message, true); } }); } function maybeRenderChart(which) { const root = state.chartRoots[which]; const payload = state.pendingChartPayloads[which]; if (!root || !payload) { return; } renderNorthIndianChartPayload(root, payload); state.pendingChartPayloads[which] = null; } function queueChart(which, payload, subtitle = "") { const shouldShowAspects = payload && ( ((state.mode === "natal" || state.mode === "child") && which === "primary" && !payload.layout_mode) || ( state.mode === "compatibility" && (which === "primary" || which === "secondary") && payload.layout_mode === "compatibility_single" ) ); const chartPayload = shouldShowAspects ? { ...payload, show_aspects: true } : state.mode === "prashna" && which === "primary" ? { ...payload, layout_mode: "prashna" } : payload; state.pendingChartPayloads[which] = chartPayload; state.renderedChartPayloads[which] = chartPayload; state.chartSubtitles[which] = subtitle; maybeRenderChart(which); if (which === "primary") { chartPrimaryCardEl?.classList.remove("hidden"); updateChartMeta(chartPrimaryMetaEl, chartPayload, subtitle); if (state.mode === "natal" || state.mode === "child") { renderNatalChakraCard(chartPayload); chakraInterpretationEl?.classList.add("hidden"); } } else if (which === "secondary") { chartSecondaryCardEl?.classList.remove("hidden"); updateChartMeta(chartSecondaryMetaEl, chartPayload, subtitle); } if (!state.restoredWorkspaceCache) { persistWorkspaceCache(); } } function queueCompatibilityOverlayChart(chartA, chartB, overlayMode = "d1") { const overlayPayload = buildCompatibilityOverlayChartPayload(chartA, chartB); if (!overlayPayload) { if (compatibilityOverlayCardEl) { compatibilityOverlayCardEl.classList.add("hidden"); } return; } if (compatibilityOverlayTitleEl) { compatibilityOverlayTitleEl.textContent = overlayMode === "moon" ? "Лунная совместимость пары" : "Совместная карта пары"; } if (compatibilityOverlaySubtitleEl) { compatibilityOverlaySubtitleEl.textContent = overlayMode === "moon" ? "Планеты женщины наложены на Лунную карту мужчины. Этот режим используется, если хотя бы одно время рождения примерное." : "Планеты женщины наложены на знаки и дома карты мужчины по Основной карте (D1)."; } compatibilityOverlayCardEl?.classList.remove("hidden"); queueChart("compatibility_overlay", overlayPayload); } function normalizeCompatibilityChartLayer(value) { return COMPATIBILITY_CHART_LAYERS[value] ? value : "D1"; } function syncCompatibilityChartSelectForTimeMode(selectEl, forceMoonMode) { if (!selectEl) { return; } Array.from(selectEl.options || []).forEach((option) => { option.disabled = Boolean(forceMoonMode && option.value !== "moon"); }); if (forceMoonMode && selectEl.value !== "moon") { selectEl.value = "moon"; } else if (!forceMoonMode && selectEl.value === "moon") { selectEl.value = "D1"; } } function updateCompatibilityTimeModeUi() { if (!isCompatibilityRoute) { return false; } const moonMode = compatibilityUsesMoonMode(); syncCompatibilityChartSelectForTimeMode(compatibilityPersonAChartSelectEl, moonMode); syncCompatibilityChartSelectForTimeMode(compatibilityPersonBChartSelectEl, moonMode); if (moonMode) { state.compatibilityChartSelections.person_a = "moon"; state.compatibilityChartSelections.person_b = "moon"; } else { state.compatibilityChartSelections.person_a = compatibilityPersonAChartSelectEl?.value || "D1"; state.compatibilityChartSelections.person_b = compatibilityPersonBChartSelectEl?.value || "D1"; } if (pairUploadNoteEl) { pairUploadNoteEl.textContent = moonMode ? "Если хотя бы одно время примерное, совместимость строится по космограмме: без лагны, домов и точных дробных карт." : "Построит две карты и общую карту пары по введённым или выбранным данным."; } return moonMode; } function getCompatibilityChartLayer(target) { const moonMode = updateCompatibilityTimeModeUi(); if (moonMode) { return "moon"; } const key = target === "person_b" ? "person_b" : "person_a"; const selectEl = key === "person_b" ? compatibilityPersonBChartSelectEl : compatibilityPersonAChartSelectEl; const selectedValue = selectEl?.value || state.compatibilityChartSelections?.[key] || "D1"; const normalizedValue = normalizeCompatibilityChartLayer(selectedValue); state.compatibilityChartSelections[key] = normalizedValue; if (selectEl && selectEl.value !== normalizedValue) { selectEl.value = normalizedValue; } return normalizedValue; } function buildCompatibilityBirthChartRequest(fields, rawChartPayload) { const target = fields === personBFields ? "b" : "a"; return { birth_date: normalizeBirthDateForBackend(fields.birth_date.value), birth_time: fields.birth_time.value, birth_place: fields.birth_place.value, birth_utc_offset: getActiveCompatibilityUtcOffset(target) || null, raw_chart_payload: rawChartPayload, }; } function normalizeNatalChartLayer(value) { return NATAL_CHART_LAYERS[value] ? value : "moon"; } function syncNatalChartLayerUi() { if (!natalChartLayerSelectEl) { return "D1"; } const exactTime = normalizeBirthTimeQuality(primaryFields.birth_time_quality?.value || "exact") === "exact"; Array.from(natalChartLayerSelectEl.options || []).forEach((option) => { option.disabled = !exactTime && !["cosmogram", "moon", "D1"].includes(option.value); }); if (exactTime && state.natalChartLayer === "D1" && natalChartLayerSelectEl.value === "moon") { natalChartLayerSelectEl.value = "D1"; } else if (exactTime && !NATAL_CHART_LAYERS[natalChartLayerSelectEl.value]) { natalChartLayerSelectEl.value = state.natalChartLayer || "D1"; } else if (!exactTime && natalChartLayerSelectEl.value !== "moon") { natalChartLayerSelectEl.value = "moon"; state.natalChartLayer = "moon"; } const layerKey = normalizeNatalChartLayer(natalChartLayerSelectEl.value || state.natalChartLayer); state.natalChartLayer = layerKey; const layer = NATAL_CHART_LAYERS[layerKey] || NATAL_CHART_LAYERS.D1; if (natalChartLayerCaptionEl) { natalChartLayerCaptionEl.textContent = exactTime ? layer.tech : `${layer.tech}. Дробные карты требуют точного времени рождения.`; } return layerKey; } function applyNatalTimeQualityDefaultLayer() { if (!isNatalRoute || !natalChartLayerSelectEl) { return syncNatalChartLayerUi(); } const exactTime = normalizeBirthTimeQuality(primaryFields.birth_time_quality?.value || "exact") === "exact"; const defaultLayer = exactTime ? "D1" : "moon"; natalChartLayerSelectEl.value = defaultLayer; state.natalChartLayer = defaultLayer; return syncNatalChartLayerUi(); } function getNatalChartLayer() { return syncNatalChartLayerUi(); } async function fetchNatalChartLayer(layerKey, subtitle) { const normalizedLayerKey = normalizeNatalChartLayer(layerKey); const layer = NATAL_CHART_LAYERS[normalizedLayerKey] || NATAL_CHART_LAYERS.D1; if (layer.type === "cosmogram") { const chartPayload = await fetchJson("/astro-context/north-indian/from-birth-data", { birth_date: normalizeBirthDateForBackend(primaryFields.birth_date.value), birth_time: primaryFields.birth_time.value, birth_place: primaryFields.birth_place.value, birth_utc_offset: getActivePrimaryUtcOffset() || null, raw_chart_payload: state.uploadedRawCharts.primary, chart_basis: "cosmogram", }); queueChart("primary", chartPayload, subtitle || `натал / ${layer.subtitle}`); chartSecondaryMetaEl.innerHTML = ""; return; } if (layer.type === "moon") { const chartPayload = await fetchJson("/astro-context/north-indian/from-birth-data", { birth_date: normalizeBirthDateForBackend(primaryFields.birth_date.value), birth_time: primaryFields.birth_time.value, birth_place: primaryFields.birth_place.value, birth_utc_offset: getActivePrimaryUtcOffset() || null, raw_chart_payload: state.uploadedRawCharts.primary, chart_basis: "moon", }); queueChart("primary", chartPayload, subtitle || `натал / ${layer.subtitle}`); chartSecondaryMetaEl.innerHTML = ""; return; } if (layer.type !== "division") { await requestSingleChart(subtitle, { ignoreNatalLayer: true }); return; } const chartPayload = await fetchJson("/astro-context/north-indian/divisional/from-birth-data", { birth_date: normalizeBirthDateForBackend(primaryFields.birth_date.value), birth_time: primaryFields.birth_time.value, birth_place: primaryFields.birth_place.value, birth_utc_offset: getActivePrimaryUtcOffset() || null, raw_chart_payload: state.uploadedRawCharts.primary, division: layer.division, }); queueChart("primary", chartPayload, subtitle || `натал / ${layer.subtitle}`); chartSecondaryMetaEl.innerHTML = ""; } async function fetchCompatibilityD1Chart(fields, rawChartPayload) { const localChart = buildChartPayloadFromRawExport(rawChartPayload); if (localChart) { return localChart; } return fetchJson("/astro-context/north-indian/from-birth-data", buildCompatibilityBirthChartRequest(fields, rawChartPayload)); } async function fetchCompatibilityChartLayer(fields, rawChartPayload, layerKey) { const normalizedLayerKey = normalizeCompatibilityChartLayer(layerKey); const layer = COMPATIBILITY_CHART_LAYERS[normalizedLayerKey]; const basePayload = buildCompatibilityBirthChartRequest(fields, rawChartPayload); if (layer.type === "lagna") { return fetchCompatibilityD1Chart(fields, rawChartPayload); } if (layer.type === "moon") { return fetchJson("/astro-context/north-indian/from-birth-data", { ...basePayload, chart_basis: "moon", }); } return fetchJson("/astro-context/north-indian/divisional/from-birth-data", { ...basePayload, division: layer.division, }); } function compatibilityLayerSubtitle(target, layerKey) { const personLabel = target === "person_b" ? "карта женщины" : "карта мужчины"; const layer = COMPATIBILITY_CHART_LAYERS[normalizeCompatibilityChartLayer(layerKey)] || COMPATIBILITY_CHART_LAYERS.D1; return `совместимость / ${personLabel} / ${layer.subtitle}`; } async function refreshCompatibilityChartsFromSelector() { if (!isCompatibilityRoute) { return; } const requiredFields = [ personAFields.birth_date, personAFields.birth_time, personAFields.birth_place, personBFields.birth_date, personBFields.birth_time, personBFields.birth_place, ]; if (requiredFields.some((field) => !String(field?.value || "").trim())) { return; } try { const requestId = ++state.compatibilityChartRequestId; setStatus("Обновляю выбранный слой карты..."); await requestCompatibilityCharts(); if (requestId !== state.compatibilityChartRequestId) { return; } setStatus("Карты пары обновлены."); } catch (error) { const message = error instanceof Error ? error.message : "Не удалось обновить выбранный слой карты."; setStatus(message, true); } } function buildCompatibilityOverlayChartPayload(chartA, chartB) { const housesA = Array.isArray(chartA?.houses) ? chartA.houses : []; const housesB = Array.isArray(chartB?.houses) ? chartB.houses : []; if (!housesA.length || !housesB.length) { return null; } const womanItemsBySign = new Map(); housesB.forEach((house) => { const sign = String(house.sign_number ?? ""); if (!sign) { return; } const items = [ ...(womanItemsBySign.get(sign) || []), ...(Array.isArray(house.natal_planets) ? house.natal_planets : []), ]; if (house.is_asc) { items.push("Asc"); } womanItemsBySign.set(sign, items); }); return { ...chartA, layout_mode: "compatibility_overlay", houses: housesA.map((house) => { const sign = String(house.sign_number ?? ""); return { ...house, natal_planets: Array.isArray(house.natal_planets) ? house.natal_planets : [], transit_planets: womanItemsBySign.get(sign) || [], }; }), }; } function withChartLayoutMode(payload, layoutMode) { if (!payload || !layoutMode) { return payload; } return { ...payload, layout_mode: layoutMode, }; } function rawChartMatchesFields(rawChartPayload, fields) { const cardData = extractSingleCard(rawChartPayload); if (!cardData) { return false; } const currentCard = buildCompatibilityCard(fields); return ( String(cardData.birth_date || cardData.date || "") === String(currentCard.birth_date || "") && String(cardData.birth_time || cardData.time || "") === String(currentCard.birth_time || "") && String(cardData.birth_place || cardData.place || "") === String(currentCard.birth_place || "") ); } async function fetchJson(url, payload) { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const rawText = await response.text(); let data = null; if (rawText) { try { data = JSON.parse(rawText); } catch { data = null; } } if (!response.ok) { const message = data?.detail ?? rawText?.trim() ?? "Запрос завершился с ошибкой."; throw new Error(message); } if (data === null) { throw new Error("Сервер вернул ответ не в JSON-формате."); } return data; } function renderPublicProductPricing(pricing = {}) { const rows = publicProductPricingLabels.map(([key, label]) => { const value = Number(pricing?.[key] ?? 0); return `
${escapeHtml(label)}${formatInteger(value)} ток.
`; }).join(""); document.querySelectorAll("[data-product-pricing-list]").forEach((list) => { list.innerHTML = rows; }); } async function loadPublicProductPricing() { if (publicProductPricingCache) { renderPublicProductPricing(publicProductPricingCache); return publicProductPricingCache; } const response = await fetch("/admin-api/public/product-pricing"); const result = await response.json().catch(() => null); if (!response.ok) { throw new Error(result?.detail || "Не удалось загрузить прайс."); } publicProductPricingCache = result?.pricing || {}; renderPublicProductPricing(publicProductPricingCache); return publicProductPricingCache; } async function togglePublicProductPricing(button) { const widget = button?.closest(".product-pricing-widget"); const panel = widget?.querySelector("[data-product-pricing-panel]"); const list = widget?.querySelector("[data-product-pricing-list]"); if (!panel || !list) { return; } const shouldOpen = !panel.classList.contains("is-open"); panel.classList.toggle("is-open", shouldOpen); button.textContent = shouldOpen ? "Скрыть прайс" : "Прайс"; if (!shouldOpen) { return; } list.textContent = "Загружаю прайс..."; try { await loadPublicProductPricing(); } catch (error) { list.textContent = error instanceof Error ? error.message : "Не удалось загрузить прайс."; } } function escapeHtml(value) { return String(value || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function textToReportHtml(value) { return escapeHtml(value).replace(/\n/g, "
"); } function getBirthPlaceAutocompleteRefs(target) { if (target === "person_a") { return { input: personAFields.birth_place, suggestionsEl: personABirthPlaceSuggestionsEl, noteEl: personABirthPlaceNoteEl, key: "person_a", dateInput: personAFields.birth_date, }; } if (target === "person_b") { return { input: personBFields.birth_place, suggestionsEl: personBBirthPlaceSuggestionsEl, noteEl: personBBirthPlaceNoteEl, key: "person_b", dateInput: personBFields.birth_date, }; } return { input: primaryFields.birth_place, suggestionsEl: primaryBirthPlaceSuggestionsEl, noteEl: primaryBirthPlaceNoteEl, key: "primary", dateInput: primaryFields.birth_date, }; } function renderBirthPlaceOptions(target, places) { const refs = getBirthPlaceAutocompleteRefs(target); state.placeOptions[refs.key] = places; if (!refs.suggestionsEl) { return; } let suggestionsHtml = places .slice(0, 6) .map((place) => ``) .join(""); const geoOption = state.placeExternalOptions[refs.key]; if (geoOption?.place) { const timezone = geoOption.timezone ? ` · UTC ${geoOption.timezone}` : ""; suggestionsHtml += ``; } const wikiQuery = state.placeWikiQueries[refs.key]; if (!suggestionsHtml && wikiQuery) { const wikiUrl = `https://ru.wikipedia.org/wiki/Special:Search?search=${encodeURIComponent(wikiQuery)}`; suggestionsHtml = `Найти населенный пункт в Википедии`; } refs.suggestionsEl.innerHTML = suggestionsHtml; refs.suggestionsEl.classList.toggle("hidden", !suggestionsHtml); } function setBirthPlaceNote(target, message, warning = false) { const refs = getBirthPlaceAutocompleteRefs(target); if (!refs.noteEl) { return; } refs.noteEl.textContent = message; refs.noteEl.style.color = warning ? "var(--danger)" : ""; } function markBirthPlaceSelected(target, placeLabel) { const refs = getBirthPlaceAutocompleteRefs(target); if (!refs.input) { return; } refs.input.dataset.selectedPlace = normalizeLocationLabel(placeLabel || refs.input.value); refs.input.dataset.placeResolved = refs.input.dataset.selectedPlace ? "true" : ""; } function isBirthPlaceAlreadySelected(target) { const refs = getBirthPlaceAutocompleteRefs(target); if (!refs.input) { return false; } const currentValue = normalizeLocationLabel(refs.input.value); const selectedValue = normalizeLocationLabel(refs.input.dataset.selectedPlace || ""); return Boolean( currentValue && selectedValue && refs.input.dataset.placeResolved === "true" && placeMatchesQuery(currentValue, selectedValue) ); } function selectBirthPlaceOption(target, placeLabel) { const refs = getBirthPlaceAutocompleteRefs(target); if (!refs.input) { return; } const geoOption = state.placeExternalOptions[refs.key]; refs.input.value = placeLabel; markBirthPlaceSelected(target, placeLabel); refs.input.dataset.selectedUtcOffset = geoOption?.place === placeLabel ? normalizeUtcOffset(geoOption.timezone) : ""; refs.suggestionsEl?.classList.add("hidden"); state.placeWikiQueries[refs.key] = ""; setBirthPlaceNote(target, `Выбрано: ${placeLabel}`); refs.input.dispatchEvent(new Event("input", { bubbles: true })); refs.input.dispatchEvent(new Event("change", { bubbles: true })); } async function resolveBirthPlaceViaGeo(target, query) { const refs = getBirthPlaceAutocompleteRefs(target); const place = normalizeLocationLabel(query); if (!place || place.length < 3) { return null; } return fetchJson("/astro-context/resolve-place", { place, reference_date: String(refs.dateInput?.value || "").trim() || null, }); } async function refreshBirthPlaceOptions(target) { if (isPrashnaRoute) { return; } const refs = getBirthPlaceAutocompleteRefs(target); if (isBirthPlaceAlreadySelected(target)) { refs.suggestionsEl?.classList.add("hidden"); setBirthPlaceNote(target, `Выбрано: ${refs.input.dataset.selectedPlace}`); return; } const query = normalizeLocationLabel(refs.input?.value || ""); if (!query || query.length < 2) { state.placeExternalOptions[refs.key] = null; state.placeWikiQueries[refs.key] = ""; renderBirthPlaceOptions(target, []); setBirthPlaceNote(target, "Начните вводить город и выберите вариант из списка. Для маленького поселка укажите ближайший город."); return; } const alreadyShown = (state.placeOptions[refs.key] || []).find((place) => placeMatchesQuery(place.place, query)); if (alreadyShown) { selectBirthPlaceOption(target, alreadyShown.place); return; } try { state.placeExternalOptions[refs.key] = null; state.placeWikiQueries[refs.key] = ""; const response = await fetch(`/astro-context/search-place?q=${encodeURIComponent(query)}&limit=10`, { method: "GET", headers: { Accept: "application/json" }, }); const payload = await response.json(); const places = Array.isArray(payload?.results) ? payload.results : []; renderBirthPlaceOptions(target, places); const exact = places.find((place) => placeMatchesQuery(place.place, query)); if (exact) { selectBirthPlaceOption(target, exact.place); } else if (places.length) { refs.input.dataset.placeResolved = ""; setBirthPlaceNote(target, "Выберите город из выпадающего списка. Для маленького поселка укажите ближайший город."); } else { try { const resolved = await resolveBirthPlaceViaGeo(target, query); state.placeExternalOptions[refs.key] = resolved?.place ? resolved : null; } catch { state.placeExternalOptions[refs.key] = null; } refs.input.dataset.placeResolved = ""; if (state.placeExternalOptions[refs.key]) { renderBirthPlaceOptions(target, []); setBirthPlaceNote(target, "В атласе не найдено. Гео нашел вариант: проверьте название и нажмите кнопку ниже, если это нужное место.", true); } else { state.placeWikiQueries[refs.key] = query; renderBirthPlaceOptions(target, []); setBirthPlaceNote(target, "Город не найден в атласе и гео. Укажите ближайший крупный город или найдите координаты в Википедии.", true); } } } catch { setBirthPlaceNote(target, "Не удалось загрузить подсказки городов. Попробуйте еще раз.", true); } } function bindBirthPlaceAutocomplete(target) { const refs = getBirthPlaceAutocompleteRefs(target); let timer = null; const schedule = () => { if (isBirthPlaceAlreadySelected(target)) { window.clearTimeout(timer); refs.suggestionsEl?.classList.add("hidden"); setBirthPlaceNote(target, `Выбрано: ${refs.input.dataset.selectedPlace}`); return; } refs.input.dataset.placeResolved = ""; window.clearTimeout(timer); timer = window.setTimeout(() => refreshBirthPlaceOptions(target), 180); }; refs.input?.addEventListener("input", () => { if (isBirthPlaceAlreadySelected(target)) { return; } const value = normalizeLocationLabel(refs.input.value); const exact = (state.placeOptions[refs.key] || []).find((place) => placeMatchesQuery(place.place, value)); if (exact) { selectBirthPlaceOption(target, exact.place); return; } schedule(); }); refs.input?.addEventListener("change", () => { if (isBirthPlaceAlreadySelected(target)) { return; } const value = normalizeLocationLabel(refs.input.value); const exact = (state.placeOptions[refs.key] || []).find((place) => placeMatchesQuery(place.place, value)); if (exact) { selectBirthPlaceOption(target, exact.place); } }); refs.input?.addEventListener("focus", schedule); refs.input?.addEventListener("keyup", schedule); refs.suggestionsEl?.addEventListener("pointerdown", (event) => { const button = event.target.closest(".place-suggestion"); if (!button || !button.dataset.place) { return; } event.preventDefault(); window.clearTimeout(timer); selectBirthPlaceOption(target, button.dataset.place || button.textContent || ""); }); } async function resolvePlaceForPrashna(place, referenceDate) { return fetchJson("/astro-context/resolve-place", { place, reference_date: referenceDate || null, }); } function normalizeUtcOffset(value) { const raw = String(value || "").trim().toUpperCase().replace(/^UTC/, "").replace(/^GMT/, "").trim(); if (!raw) { return ""; } if (["Z", "0", "+0", "-0", "00", "+00", "-00", "+00:00", "-00:00"].includes(raw)) { return "+00:00"; } const signed = raw.startsWith("+") || raw.startsWith("-") ? raw : `+${raw}`; const compact = signed.replace(/\s+/g, ""); const shortMatch = compact.match(/^([+-])(\d{1,2})$/); if (shortMatch) { return `${shortMatch[1]}${shortMatch[2].padStart(2, "0")}:00`; } const match = compact.match(/^([+-])(\d{1,2}):?(\d{2})$/); if (!match) { return ""; } const hours = Number(match[2]); const minutes = Number(match[3]); if (hours > 14 || minutes > 59) { return ""; } return `${match[1]}${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`; } async function resolveUtcOffsetForBirthCard(card) { const matchingPlaceFields = [primaryFields.birth_place, personAFields.birth_place, personBFields.birth_place]; const matchedField = matchingPlaceFields.find((field) => ( placeMatchesQuery(field?.value || "", card.birth_place) && placeMatchesQuery(field?.dataset?.selectedPlace || "", card.birth_place) )); const selectedOffset = normalizeUtcOffset(matchedField?.dataset?.selectedUtcOffset || ""); if (selectedOffset) { return selectedOffset; } const result = await fetchJson("/astro-context/normalized/from-birth-data", { birth_date: normalizeBirthDateForBackend(card.birth_date), birth_time: card.birth_time, birth_place: card.birth_place, }); const offset = normalizeUtcOffset(result?.birth_info?.timezone); if (!offset) { throw new Error("Не удалось автоматически определить UTC для карты."); } return offset; } function showPrashnaPlaceStatus(message, isError = false) { if (!prashnaPlaceStatusEl) { return; } if (!message) { prashnaPlaceStatusEl.textContent = ""; prashnaPlaceStatusEl.classList.add("hidden"); prashnaPlaceStatusEl.classList.remove("error"); return; } prashnaPlaceStatusEl.textContent = message; prashnaPlaceStatusEl.classList.remove("hidden"); prashnaPlaceStatusEl.classList.toggle("error", Boolean(isError)); } function buildTransitReference() { const transitDate = transitFields.transit_date.value.trim(); const transitTime = transitFields.transit_time.value.trim(); const transitTimezone = transitFields.transit_timezone.value.trim(); const transitPlace = transitFields.transit_place.value.trim(); const transitLatitude = transitFields.transit_latitude.value.trim(); const transitLongitude = transitFields.transit_longitude.value.trim(); if (!transitDate || !transitTime) { return null; } return { transit_mode: "custom_datetime", transit_date: transitDate, transit_time: transitTime, transit_timezone: transitTimezone || null, transit_place: transitPlace || null, transit_latitude: transitLatitude ? Number(transitLatitude) : null, transit_longitude: transitLongitude ? Number(transitLongitude) : null, transit_location_source: "manual_place", }; } function updateTransitsForecastControls() { const isForecast = state.transitsForecastMode === "forecast"; transitsForecastPanelEl?.classList.toggle("hidden", state.mode !== "transits"); transitsForecastNoteEl?.classList.toggle("hidden", !isForecast); transitsForecastPanelEl?.querySelectorAll("[data-transits-forecast-mode]").forEach((button) => { button.classList.toggle("is-active", button.dataset.transitsForecastMode === state.transitsForecastMode); }); } function isTransitsForecastRequest() { return state.mode === "transits" && state.transitsForecastMode === "forecast"; } function buildTransitsForecastPayload(includePreviousContext) { const payload = buildChatPayload(includePreviousContext); const inferred = inferTransitsForecastRequest(getCurrentQuestionText()); payload.forecast_topic = inferred.topic; payload.forecast_period = inferred.period; payload.forecast_start_date = transitFields.transit_date.value.trim() || null; payload.forecast_end_date = null; payload.answer_scope = "focused"; return payload; } function inferTransitsForecastRequest(questionText) { const text = String(questionText || "").toLowerCase().replaceAll("ё", "е"); return { topic: inferForecastTopicFromText(text), period: inferForecastPeriodFromText(text), }; } function inferForecastTopicFromText(text) { const topicPatterns = [ ["relationships", ["личн", "любов", "отношен", "партнер", "партнер", "мужчин", "женщин", "брак", "замуж", "жениться"]], ["work", ["работ", "карьер", "должност", "началь", "профес", "увол", "сменить работу", "проект"]], ["money", ["деньг", "финанс", "доход", "заработ", "кредит", "долг", "покуп", "расход"]], ["family", ["семь", "родител", "дет", "дом", "квартир", "родствен"]], ["health", ["здоров", "самочув", "сил", "энерг", "болез", "врач"]], ["study", ["учеб", "обуч", "экзам", "курс", "университет", "школ", "знани"]], ]; const match = topicPatterns.find(([, patterns]) => patterns.some((pattern) => text.includes(pattern))); return match?.[0] || "general"; } function inferForecastPeriodFromText(text) { if (text.includes("до конца года") || text.includes("до конца этого года") || text.includes("до конца 2026")) { return "end_of_year"; } if (/\b(3|три)\s*(месяц|мес)/.test(text)) { return "3m"; } if (/\b(6|шесть|полгода|пол года)\s*(месяц|мес)?/.test(text)) { return "6m"; } if (text.includes("12 месяц") || text.includes("двенадцать месяц") || text.includes("год")) { return "12m"; } return "end_of_year"; } async function applyResolvedPrashnaPlace(placeText) { const normalizedPlace = String(placeText || "").trim(); if (!normalizedPlace || (state.mode !== "prashna" && state.mode !== "transits")) { return; } try { const resolved = await resolvePlaceForPrashna( normalizedPlace, transitFields.transit_date.value.trim() || null, ); if (state.mode !== "prashna" && state.mode !== "transits") { return; } if (transitFields.transit_place.value.trim() !== normalizedPlace) { return; } const resolvedLabel = normalizeLocationLabel(resolved?.place || normalizedPlace); transitFields.transit_place.value = resolvedLabel; if (typeof resolved?.latitude === "number") { transitFields.transit_latitude.value = formatCoordinateValue(resolved.latitude); } if (typeof resolved?.longitude === "number") { transitFields.transit_longitude.value = formatCoordinateValue(resolved.longitude); } if (typeof resolved?.timezone === "string" && resolved.timezone.trim()) { transitFields.transit_timezone.value = resolved.timezone.trim(); } showPrashnaPlaceStatus("Координаты обновлены."); } catch { showPrashnaPlaceStatus("Не удалось обновить координаты. Введите координаты вручную.", true); } } function schedulePrashnaPlaceResolve() { if (state.mode !== "prashna" && state.mode !== "transits") { return; } const placeText = transitFields.transit_place.value.trim(); if (!placeText) { return; } if (state.prashnaPlaceResolveTimer) { window.clearTimeout(state.prashnaPlaceResolveTimer); } state.prashnaPlaceResolveTimer = window.setTimeout(() => { state.prashnaPlaceResolveTimer = null; applyResolvedPrashnaPlace(placeText); }, 650); } function buildChatPayload(includePreviousContext) { const isPrashnaMode = state.mode === "prashna"; const resolvedQuestion = getCurrentQuestionText(); const resolvedBirthDate = primaryFields.birth_date.value || (isPrashnaMode ? transitFields.transit_date.value : ""); const resolvedBirthTime = primaryFields.birth_time.value || (isPrashnaMode ? transitFields.transit_time.value : ""); const resolvedBirthPlace = primaryFields.birth_place.value || (isPrashnaMode ? transitFields.transit_place.value : ""); const clientUserId = state.clientReferral.profile?.user_id || state.clientProfile?.user_id || null; const isNatalFullAnalysisRequest = state.mode === "natal" && Boolean(state.pendingNatalQuestion) && !String(questionEl?.value || "").trim() && !state.forceAnalysisFollowup; const questionMode = state.mode === "natal" ? (isNatalFullAnalysisRequest ? "initial" : "followup") : (state.forceAnalysisFollowup ? "followup" : "initial"); const answerScope = state.forceAnalysisFollowup ? "followup" : (isNatalFullAnalysisRequest ? "full" : "focused"); const payload = { question: resolvedQuestion, question_mode: questionMode, answer_scope: answerScope, client_name: (clientNameEl?.value || "").trim() || null, client_gender: (clientGenderEl?.value || "").trim() || null, client_user_id: clientUserId, birth_date: resolvedBirthDate, birth_time: resolvedBirthTime, birth_time_quality: normalizeBirthTimeQuality(primaryFields.birth_time_quality?.value || "exact"), birth_place: resolvedBirthPlace, birth_utc_offset: isPrashnaMode ? null : (getActivePrimaryUtcOffset() || null), raw_chart_payload: state.uploadedRawCharts.primary, ...readTrafficSourceMeta(), }; if (state.admin.authenticated) { payload.admin_mode = true; payload.admin_token = state.admin.token; } if (state.mode === "transits" || state.mode === "prashna") { payload.transit_reference = buildTransitReference(); } if (state.mode === "transits") { payload.transit_raw_chart_payload = state.uploadedRawCharts.transit; } if ( includePreviousContext && state.lastChatDebugContext && state.lastChatInputSignature === buildCurrentInputSignature() ) { payload.previous_context = state.lastChatDebugContext; payload.answer_scope = "followup"; } return payload; } function buildReusableContext(result) { const prashnaContext = result?.prashna_context && typeof result.prashna_context === "object" ? result.prashna_context : null; const prashnaTopic = prashnaContext?.question_context?.topic ?? prashnaContext?.topic ?? null; return { scenario: result?.scenario_name ?? null, scenario_name: result?.scenario_name ?? null, question_type: result?.question_type ?? null, question: result?.question ?? getCurrentQuestionText(), answer: result?.answer ?? "", topic: prashnaTopic, prashna_context: prashnaContext, last_prashna_context: prashnaContext, compatibility_topic: result?.compatibility_topic ?? null, redirect_target: result?.redirect_target ?? null, redirect_reason: result?.redirect_reason ?? null, }; } function buildCompatibilityPayload(includePreviousContext = false) { const clientUserId = state.clientReferral.profile?.user_id || state.clientProfile?.user_id || null; const isFullAnalysisRequest = Boolean(state.pendingCompatibilityQuestion) && !String(questionEl?.value || "").trim() && !state.forceCompatibilityFollowup; const payload = { question: getCurrentQuestionText(), question_mode: isFullAnalysisRequest ? "initial" : "followup", answer_scope: isFullAnalysisRequest ? "full" : "focused", client_name: (clientNameEl?.value || "").trim() || null, client_gender: (clientGenderEl?.value || "").trim() || null, client_user_id: clientUserId, person_a_gender: personAFields.card_gender.value, person_a_birth_date: normalizeBirthDateForBackend(personAFields.birth_date.value), person_a_birth_time: personAFields.birth_time.value, person_a_birth_time_quality: normalizeBirthTimeQuality(personAFields.birth_time_quality?.value || "exact"), person_a_birth_place: personAFields.birth_place.value, person_a_birth_utc_offset: getActiveCompatibilityUtcOffset("a") || null, person_a_raw_chart_payload: state.uploadedRawCharts.person_a, person_b_gender: personBFields.card_gender.value, person_b_birth_date: normalizeBirthDateForBackend(personBFields.birth_date.value), person_b_birth_time: personBFields.birth_time.value, person_b_birth_time_quality: normalizeBirthTimeQuality(personBFields.birth_time_quality?.value || "exact"), person_b_birth_place: personBFields.birth_place.value, person_b_birth_utc_offset: getActiveCompatibilityUtcOffset("b") || null, person_b_raw_chart_payload: state.uploadedRawCharts.person_b, compatibility_time_mode: compatibilityUsesMoonMode() ? "moon" : "exact", ...readTrafficSourceMeta(), }; if (state.admin.authenticated) { payload.admin_mode = true; payload.admin_token = state.admin.token; } if ( includePreviousContext && state.lastChatDebugContext && state.lastChatInputSignature === buildCurrentInputSignature() ) { payload.previous_context = state.lastChatDebugContext; payload.question_mode = "followup"; payload.answer_scope = "followup"; payload.previous_answer_summary = state.lastChatDebugContext.answer ?? null; } return payload; } async function requestSingleChart(subtitle, options = {}) { if (!options.ignoreNatalLayer && (state.mode === "natal" || state.mode === "child")) { const layerKey = getNatalChartLayer(); const layer = NATAL_CHART_LAYERS[layerKey] || NATAL_CHART_LAYERS.D1; if (layer.type === "division" || layer.type === "cosmogram" || layer.type === "moon") { await fetchNatalChartLayer(layerKey, subtitle || `натал / ${layer.subtitle}`); return; } } if (state.mode === "transits" && state.uploadedRawCharts.primary && state.uploadedRawCharts.transit) { const shouldUseMoonTransitsChart = normalizeBirthTimeQuality(primaryFields.birth_time_quality?.value || "exact") === "approximate"; const chartPayload = await fetchJson("/astro-context/north-indian/from-birth-data", { birth_date: normalizeBirthDateForBackend(primaryFields.birth_date.value), birth_time: primaryFields.birth_time.value, birth_place: primaryFields.birth_place.value, birth_utc_offset: getActivePrimaryUtcOffset() || null, raw_chart_payload: state.uploadedRawCharts.primary, transit_raw_chart_payload: state.uploadedRawCharts.transit, transit_reference: buildTransitReference(), chart_basis: shouldUseMoonTransitsChart ? "moon" : "lagna", }); queueChart("primary", chartPayload, subtitle); chartSecondaryMetaEl.innerHTML = ""; await requestTransitTimelinePanels(); return; } const shouldUseMoonNatalChart = (state.mode === "auto" || state.mode === "natal") && normalizeBirthTimeQuality(primaryFields.birth_time_quality?.value || "exact") === "approximate"; if (shouldUseMoonNatalChart && state.uploadedRawCharts.primary) { const chartPayload = await fetchJson("/astro-context/north-indian/from-birth-data", { birth_date: normalizeBirthDateForBackend(primaryFields.birth_date.value), birth_time: primaryFields.birth_time.value, birth_place: primaryFields.birth_place.value, birth_utc_offset: getActivePrimaryUtcOffset() || null, raw_chart_payload: state.uploadedRawCharts.primary, chart_basis: "moon", }); queueChart("primary", chartPayload, subtitle); chartSecondaryMetaEl.innerHTML = ""; return; } const localChartPayload = buildChartPayloadFromRawExport(state.uploadedRawCharts.primary); if (localChartPayload) { queueChart("primary", localChartPayload, subtitle); chartSecondaryMetaEl.innerHTML = ""; if (state.mode === "transits") { await requestTransitTimelinePanels(); } return; } const isPrashnaMode = state.mode === "prashna"; if (isPrashnaMode) { const prashnaChartPayload = buildChartPayloadFromNormalizedContext( state.lastWorkspaceResult?.prashna_context?.prashna_base, ); if (prashnaChartPayload) { queueChart("primary", prashnaChartPayload, subtitle || "прашна / карта момента вопроса"); chartSecondaryMetaEl.innerHTML = ""; return; } } const shouldUseMoonChart = (state.mode === "auto" || state.mode === "natal" || state.mode === "transits") && normalizeBirthTimeQuality(primaryFields.birth_time_quality?.value || "exact") === "approximate"; const payload = { birth_date: normalizeBirthDateForBackend( isPrashnaMode ? transitFields.transit_date.value : primaryFields.birth_date.value, ), birth_time: isPrashnaMode ? transitFields.transit_time.value : primaryFields.birth_time.value, birth_place: isPrashnaMode ? transitFields.transit_place.value : primaryFields.birth_place.value, birth_utc_offset: isPrashnaMode ? null : (getActivePrimaryUtcOffset() || null), raw_chart_payload: state.uploadedRawCharts.primary, }; if (shouldUseMoonChart) { payload.chart_basis = "moon"; } if (state.mode === "transits") { payload.transit_reference = buildTransitReference(); } if (state.mode === "transits") { payload.transit_raw_chart_payload = state.uploadedRawCharts.transit; } const chartPayload = await fetchJson("/astro-context/north-indian/from-birth-data", payload); queueChart("primary", chartPayload, subtitle); chartSecondaryMetaEl.innerHTML = ""; if (state.mode === "transits") { await requestTransitTimelinePanels(); } } async function requestNatalChakraInterpretation(force = false) { if (state.mode !== "natal" && state.mode !== "child") { return; } const chakraSignature = buildCurrentChakraSignature(); if (!force && chakraSignature === state.lastChakraInputSignature && state.lastChakraAnswer) { setChakraInterpretation(state.lastChakraAnswer, false); return; } setChakraInterpretation("Собираю короткую интерпретацию по чакрам...", true); try { const result = await fetchJson("/natal-v2/chakra", { birth_date: normalizeBirthDateForBackend(primaryFields.birth_date.value), birth_time: primaryFields.birth_time.value, birth_place: primaryFields.birth_place.value, birth_utc_offset: getActivePrimaryUtcOffset() || null, raw_chart_payload: state.uploadedRawCharts.primary, }); const answer = result?.answer || "Интерпретация по чакрам пока не получена."; state.lastChakraInputSignature = chakraSignature; state.lastChakraAnswer = answer; setChakraInterpretation(answer, false); persistWorkspaceCache(); } catch (error) { clearChakraCache(); setChakraInterpretation( error instanceof Error ? error.message : "Не удалось получить интерпретацию по чакрам.", true, ); } } async function requestTransitDetails(force = false, requestedMode = null) { if (state.mode !== "transits") { return; } const mode = requestedMode || state.activeTransitDetailsMode || "sign"; const detailsSignature = buildCurrentTransitDetailsSignature(); const existingRows = Array.isArray(state.lastTransitDetailsPayload?.[`by_${mode}`]) ? state.lastTransitDetailsPayload[`by_${mode}`] : []; if (!force && detailsSignature === state.lastTransitDetailsSignature && existingRows.length) { renderTransitDetails(); return; } setTransitDetailsStatus("Собираю точные периоды по транзитам..."); try { const result = await fetchJson("/transits-v2/details", { birth_date: normalizeBirthDateForBackend(primaryFields.birth_date.value), birth_time: primaryFields.birth_time.value, birth_place: primaryFields.birth_place.value, birth_utc_offset: getActivePrimaryUtcOffset() || null, raw_chart_payload: state.uploadedRawCharts.primary, transit_raw_chart_payload: state.uploadedRawCharts.transit, transit_reference: buildTransitReference(), detail_modes: [mode], }); state.lastTransitDetailsSignature = detailsSignature; state.lastTransitDetailsPayload = { ...(state.lastTransitDetailsPayload || { by_sign: [], by_nakshatra: [], by_motion: [] }), by_sign: mode === "sign" ? (result.by_sign || []) : (state.lastTransitDetailsPayload?.by_sign || []), by_nakshatra: mode === "nakshatra" ? (result.by_nakshatra || []) : (state.lastTransitDetailsPayload?.by_nakshatra || []), by_motion: mode === "motion" ? (result.by_motion || []) : (state.lastTransitDetailsPayload?.by_motion || []), }; renderTransitDetails(); persistWorkspaceCache(); } catch (error) { clearTransitDetailsCache(); setTransitDetailsStatus( error instanceof Error ? error.message : "Не удалось получить детали транзитов.", ); } } async function requestTransitDasha(force = false) { if (state.mode !== "transits") { return; } if (normalizeBirthTimeQuality(primaryFields.birth_time_quality?.value || "exact") === "approximate") { clearTransitDashaCache(); syncTransitDashaButtonState(); setTransitDashaStatus("При примерном времени Вимшоттари-тайминг не используется как надёжная основа."); return; } const dashaSignature = buildCurrentTransitDashaSignature(); const existingRows = Array.isArray(state.lastTransitDashaPayload?.periods) ? state.lastTransitDashaPayload.periods : []; if (!force && dashaSignature === state.lastTransitDashaSignature && existingRows.length) { renderTransitDasha(); return; } setTransitDashaStatus("Собираю Вимшоттари дашу..."); try { const result = await fetchJson("/transits-v2/dasha", { birth_date: normalizeBirthDateForBackend(primaryFields.birth_date.value), birth_time: primaryFields.birth_time.value, birth_time_quality: normalizeBirthTimeQuality(primaryFields.birth_time_quality?.value || "exact"), birth_place: primaryFields.birth_place.value, birth_utc_offset: getActivePrimaryUtcOffset() || null, raw_chart_payload: state.uploadedRawCharts.primary, transit_reference: buildTransitReference(), }); state.lastTransitDashaSignature = dashaSignature; state.lastTransitDashaPayload = result || { periods: [] }; renderTransitDasha(); persistWorkspaceCache(); } catch (error) { clearTransitDashaCache(); setTransitDashaStatus( error instanceof Error ? error.message : "Не удалось получить Вимшоттари дашу.", ); } } async function requestTransitTimelinePanels() { if (state.mode !== "transits") { return; } if (normalizeBirthTimeQuality(primaryFields.birth_time_quality?.value || "exact") === "approximate") { clearTransitDetailsCache(); setTransitDetailsStatus("При примерном времени точные периоды по домам не строятся. Разбор идёт от Луны и планет в знаках."); setTransitDashaStatus("При примерном времени Вимшоттари-тайминг не используется как надёжная основа."); return; } state.activeTransitDetailsMode = "sign"; await requestTransitDetails(false, "sign"); } async function runNatalChakraTest() { if (state.mode !== "natal" && state.mode !== "child") { return; } setStatus("Готовлю короткую интерпретацию чакр без полного натального разбора..."); try { await requestSingleChart("natal / chakra-only test"); await requestNatalChakraInterpretation(false); setStatus("Короткая интерпретация чакр готова."); } catch (error) { setStatus( error instanceof Error ? error.message : "Не удалось обновить блок чакр.", true, ); } } async function requestCompatibilityCharts() { const currentRawA = rawChartMatchesFields(state.uploadedRawCharts.person_a, personAFields) ? state.uploadedRawCharts.person_a : null; const currentRawB = rawChartMatchesFields(state.uploadedRawCharts.person_b, personBFields) ? state.uploadedRawCharts.person_b : null; const moonMode = updateCompatibilityTimeModeUi(); const selectedLayerA = getCompatibilityChartLayer("person_a"); const selectedLayerB = getCompatibilityChartLayer("person_b"); if (moonMode) { const [moonChartA, moonChartB] = await Promise.all([ fetchCompatibilityChartLayer(personAFields, currentRawA, "moon"), fetchCompatibilityChartLayer(personBFields, currentRawB, "moon"), ]); queueChart( "primary", withChartLayoutMode(moonChartA, "compatibility_single"), compatibilityLayerSubtitle("person_a", "moon"), ); queueChart( "secondary", withChartLayoutMode(moonChartB, "compatibility_single"), compatibilityLayerSubtitle("person_b", "moon"), ); queueCompatibilityOverlayChart(moonChartA, moonChartB, "moon"); return; } const [d1ChartA, d1ChartB] = await Promise.all([ fetchCompatibilityD1Chart(personAFields, currentRawA), fetchCompatibilityD1Chart(personBFields, currentRawB), ]); const [chartA, chartB] = await Promise.all([ selectedLayerA === "D1" ? d1ChartA : fetchCompatibilityChartLayer(personAFields, currentRawA, selectedLayerA), selectedLayerB === "D1" ? d1ChartB : fetchCompatibilityChartLayer(personBFields, currentRawB, selectedLayerB), ]); queueChart( "primary", withChartLayoutMode(chartA, "compatibility_single"), compatibilityLayerSubtitle("person_a", selectedLayerA), ); queueChart( "secondary", withChartLayoutMode(chartB, "compatibility_single"), compatibilityLayerSubtitle("person_b", selectedLayerB), ); const overlayIsMoon = selectedLayerA === "moon" && selectedLayerB === "moon"; queueCompatibilityOverlayChart( overlayIsMoon ? chartA : d1ChartA, overlayIsMoon ? chartB : d1ChartB, overlayIsMoon ? "moon" : "d1", ); } function pushHistory(result) { state.history.push({ modeLabel: modeLabels[state.mode] ?? state.mode, questionType: result.question_type ?? "-", scenarioName: result.scenario_name ?? "-", promptName: result.prompt_name ?? "-", question: result.question ?? getCurrentQuestionText(), answerPreview: (result.answer ?? "").slice(0, 180) || "Без ответа", }); renderHistory(); } function setRequestInFlight(inFlight) { state.requestInFlight = Boolean(inFlight); if (runAnalysisButton) { if (inFlight) { state.requestButtonText = runAnalysisButton.textContent || ""; runAnalysisButton.disabled = true; runAnalysisButton.textContent = "Отправляю..."; } else { runAnalysisButton.disabled = false; runAnalysisButton.textContent = "Отправить вопрос"; runAnalysisButton.setAttribute("aria-label", "Отправить вопрос"); runAnalysisButton.title = "Отправить вопрос"; state.requestButtonText = ""; } } if (runResponseButton) { runResponseButton.disabled = Boolean(inFlight); } if (analysisFollowupSubmitEl) { analysisFollowupSubmitEl.disabled = Boolean(inFlight); } } async function runRequest(debugMode) { if (state.requestInFlight) { return; } const question = getCurrentQuestionText(); if (!question) { setStatus("Сначала введите вопрос.", true); return; } setStatus("Уточняю, какой метод даст точный ответ..."); const navigatorAllowsCurrentProduct = await checkQuestionWithProductNavigator(question); if (!navigatorAllowsCurrentProduct) { setStatus("Вы можете уточнить вопрос или выбрать предложенный путь."); return; } if (state.mode === "transits" && isTransitsRoute) { reachYandexGoal("full_transits_click", { source: "run_analysis_button", }); } if (state.mode === "prashna" && isPrashnaRoute) { reachYandexGoal("full_prashna_click", { source: "run_analysis_button", }); } const productQuestionModes = new Set(["natal", "child", "transits", "compatibility", "prashna"]); const shouldClearQuestionAfterSubmit = productQuestionModes.has(state.mode) && Boolean(questionEl?.value.trim()); if (state.mode === "auto" && !state.autoSelectedDemo) { const route = detectAutoRecommendedBlock(question); if (route !== "natal") { navigateWithPrefill(route, question); setStatus(`Вопрос распознан как ${modeLabels[route]}. Перевожу в нужный блок.`); return; } } setStatus("Анализирую карту..."); setRequestInFlight(true); const requestStartedAt = performance.now(); try { let result; let reusedContextFallback = false; let chartErrorMessage = ""; if (state.mode === "compatibility") { const endpoint = debugMode ? "/compatibility-v2/chat/debug" : "/compatibility-v2/chat"; const shouldUsePreviousContext = reuseToggleEl.checked || state.forceCompatibilityFollowup; try { result = await fetchJson(endpoint, buildCompatibilityPayload(shouldUsePreviousContext)); } catch (error) { state.forceCompatibilityFollowup = false; if (!shouldUsePreviousContext || !state.lastChatDebugContext) { throw error; } clearReusableContext(); reusedContextFallback = true; result = await fetchJson(endpoint, buildCompatibilityPayload(false)); } state.forceCompatibilityFollowup = false; state.lastWorkspaceResult = result; renderServiceInfo(result); renderAnswer(result); scrollToResultOnMobile(); renderDebug(result); try { await requestCompatibilityCharts(); } catch (chartError) { chartErrorMessage = chartError instanceof Error ? chartError.message : "Не удалось построить карту совместимости."; } state.lastChatDebugContext = buildReusableContext(result); state.lastChatInputSignature = buildCurrentInputSignature(); renderCompatibilitySuggestedQuestions(); renderAnalysisFollowup(); } else { const endpointMap = { auto: debugMode ? "/natal-v2/chat/debug" : "/natal-v2/chat", natal: debugMode ? "/natal-v2/chat/debug" : "/natal-v2/chat", child: debugMode ? "/natal-v2/chat/debug" : "/natal-v2/chat", transits: debugMode ? "/transits-v2/chat/debug" : "/transits-v2/chat", prashna: debugMode ? "/prashna-v2/chat/debug" : "/prashna-v2/chat", }; const endpoint = isTransitsForecastRequest() ? "/transits-v2/forecast" : (endpointMap[state.mode] ?? (debugMode ? "/natal-v2/chat/debug" : "/natal-v2/chat")); const shouldUsePreviousContext = Boolean( (debugMode && reuseToggleEl.checked) || state.forceAnalysisFollowup ); try { result = await fetchJson( endpoint, isTransitsForecastRequest() ? buildTransitsForecastPayload(shouldUsePreviousContext) : buildChatPayload(shouldUsePreviousContext), ); } catch (error) { state.forceAnalysisFollowup = false; if (!shouldUsePreviousContext || !state.lastChatDebugContext) { throw error; } clearReusableContext(); reusedContextFallback = true; result = await fetchJson( endpoint, isTransitsForecastRequest() ? buildTransitsForecastPayload(false) : buildChatPayload(false), ); } state.forceAnalysisFollowup = false; state.lastWorkspaceResult = result; renderServiceInfo(result); renderAnswer(result); scrollToResultOnMobile(); renderDebug(result); try { await requestSingleChart( state.mode === "transits" ? "транзиты / натальная карта + наложение транзитов" : state.mode === "prashna" ? "прашна / карта момента вопроса" : `${modeLabels[state.mode] ?? state.mode} / одна карта`, ); if (state.mode === "transits") { await requestTransitTimelinePanels(); } } catch (chartError) { chartErrorMessage = chartError instanceof Error ? chartError.message : "Не удалось построить карту по введённым данным."; } if (debugMode || state.mode === "natal" || state.mode === "transits" || state.mode === "prashna") { state.lastChatDebugContext = buildReusableContext(result); state.lastChatInputSignature = buildCurrentInputSignature(); } renderAnalysisFollowup(); } pushHistory(result); if (shouldClearQuestionAfterSubmit && questionEl) { questionEl.value = ""; questionEl.dispatchEvent(new Event("input", { bubbles: true })); } persistWorkspaceCache(); const profileWasAutoCreated = await syncCreatedClientProfile(result); if (state.admin.authenticated) { await refreshAdminMetricsSummary({ silent: true }); } const durationLabel = formatThinkingDuration(performance.now() - requestStartedAt); const baseStatus = reusedContextFallback ? `Ответ и карта обновлены. Думал ${durationLabel}. Предыдущий контекст продолжения был автоматически сброшен.${profileWasAutoCreated ? " Профиль клиента подготовлен автоматически." : ""}` : `Ответ и карта обновлены. Думал ${durationLabel}.${profileWasAutoCreated ? " Профиль клиента подготовлен автоматически." : ""}`; setStatus( chartErrorMessage ? `${baseStatus} Ответ сохранён, но с построением карты возникла проблема: ${chartErrorMessage}` : baseStatus ); } catch (error) { const message = error instanceof Error ? error.message : "Неизвестная ошибка"; const previousContextWasEligible = state.mode === "compatibility" || debugMode; if (previousContextWasEligible && reuseToggleEl.checked && state.lastChatDebugContext) { clearReusableContext(); } clearAutoNextStep(); clearAnalysisFollowup(); setStatus(message, true); } finally { setRequestInFlight(false); } } modeButtons.forEach((button) => { button.addEventListener("click", () => { state.mode = button.dataset.mode; clearReusableContext("Контекст предыдущего разбора сброшен: изменён режим анализа."); clearAnalysisFollowup(); updateModeUi(); applyHomeAutoPresentation(); fillExampleQuestion(); setStatus("Режим переключён."); }); }); heroStartButtonEl?.addEventListener("click", () => { if (isProfileRoute) { window.location.href = "/"; return; } smoothScrollToElement(workspacePanelEl); questionEl?.focus(); }); autoDemoButtons.forEach((button) => { button.addEventListener("click", () => { const question = button.dataset.autoQuestion || ""; setActiveAutoDemo(button.dataset.autoDemo || ""); questionEl.value = question; clearAutoNextStep(); questionEl.focus(); setStatus("Демо-вопрос выбран. Можно сразу получить первый ответ по карте."); }); }); questionEl?.addEventListener("input", () => { const currentValue = questionEl.value.trim(); const matchedDemoEntry = Object.entries(autoDemoQuestions).find(([, value]) => value === currentValue); if (matchedDemoEntry) { setActiveAutoDemo(matchedDemoEntry[0]); return; } clearAutoDemoSelection(); }); function handleAutoNextStepAction(buttonEl) { if (state.requestInFlight) { return; } const followupQuestion = buttonEl?.dataset?.followupQuestion || ""; if (followupQuestion) { if (!state.lastChatDebugContext) { setStatus("Сначала нужен основной разбор, чтобы задать уточняющий вопрос.", true); return; } if (questionEl) { questionEl.value = followupQuestion; } state.forceCompatibilityFollowup = true; runAnalysisButton?.click(); return; } const route = buttonEl?.dataset?.targetRoute || ""; const prefillQuestion = buttonEl?.dataset?.prefillQuestion || ""; if (!route || !prefillQuestion) { return; } navigateWithPrefill(route, prefillQuestion); } function submitAnalysisFollowup() { if (state.requestInFlight) { return; } const followupQuestion = (analysisFollowupQuestionEl?.value || "").trim(); if (!followupQuestion) { setStatus("Сначала напишите уточняющий вопрос.", true); analysisFollowupQuestionEl?.focus(); return; } if (!shouldShowAnalysisFollowup()) { setStatus("Уточняющие вопросы здесь доступны после разбора Натала, Транзитов или Совместимости.", true); return; } if (questionEl) { questionEl.value = followupQuestion; questionEl.dispatchEvent(new Event("input", { bubbles: true })); } if (state.mode === "compatibility") { state.forceCompatibilityFollowup = true; } else { state.forceAnalysisFollowup = true; } analysisFollowupQuestionEl.value = ""; runAnalysisButton?.click(); } autoNextStepButtonEl?.addEventListener("click", () => { handleAutoNextStepAction(autoNextStepButtonEl); }); autoNextStepQuestionEl?.addEventListener("click", () => { handleAutoNextStepAction(autoNextStepQuestionEl); }); autoNextStepActionsEl?.addEventListener("click", (event) => { const button = event.target.closest("button"); if (!button || !autoNextStepActionsEl.contains(button)) { return; } handleAutoNextStepAction(button); }); analysisFollowupSubmitEl?.addEventListener("click", submitAnalysisFollowup); analysisFollowupQuestionEl?.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); submitAnalysisFollowup(); } }); window.addEventListener("paid-voice-transcribed", (event) => { const detail = event.detail || {}; const shouldMirrorQuestionVoiceToFollowup = detail.target_id === "question" && shouldShowAnalysisFollowup() && !analysisFollowupPanelEl?.classList.contains("hidden"); if (detail.target_id !== "analysis-followup-question" && !shouldMirrorQuestionVoiceToFollowup) { return; } const recognizedText = String(detail.text || "").trim(); if (!recognizedText || !analysisFollowupQuestionEl) { return; } const followupValue = String(analysisFollowupQuestionEl.value || "").trim(); if (!followupValue.includes(recognizedText)) { analysisFollowupQuestionEl.value = followupValue ? `${followupValue}\n${recognizedText}` : recognizedText; } if (questionEl?.value.trim() === recognizedText) { questionEl.value = ""; questionEl.dispatchEvent(new Event("input", { bubbles: true })); } analysisFollowupQuestionEl.focus(); }); workspaceProfileActionEl?.addEventListener("click", () => { window.location.href = "/profile"; }); heroProfileLinkEl?.addEventListener("click", () => { window.location.href = isProfileRoute ? "/" : "/profile"; }); mobileSectionsToggleEl?.addEventListener("click", () => { toggleMobileSections(); }); mobileSectionsMenuEl?.addEventListener("click", (event) => { if (event.target.closest("a")) { toggleMobileSections(false); } }); heroFormatsButtonEl?.addEventListener("click", () => { if (isProfileRoute) { window.location.href = "/"; return; } if (isPrashnaRoute || isNatalRoute || isChildAstrologyRoute || isTransitsRoute || isCompatibilityRoute) { window.location.href = "/"; return; } smoothScrollToElement(formatsShowcaseEl); }); showcaseModeButtons.forEach((button) => { button.addEventListener("click", () => { const nextMode = button.dataset.showcaseMode; if (nextMode === "prashna") { window.location.href = "/prashna"; return; } if (nextMode === "natal") { window.location.href = "/natal"; return; } if (nextMode === "child") { window.location.href = "/child-astrology"; return; } if (nextMode === "transits") { window.location.href = "/transits"; return; } if (nextMode === "compatibility") { window.location.href = "/compatibility"; return; } const targetButton = modeButtons.find((item) => item.dataset.mode === nextMode); targetButton?.click(); smoothScrollToElement(workspacePanelEl); }); }); bindFieldReuseReset(primaryFields, "Контекст предыдущего разбора сброшен: изменены данные карты."); bindFieldReuseReset(personAFields, "Контекст предыдущего разбора сброшен: изменены данные пары."); bindFieldReuseReset(personBFields, "Контекст предыдущего разбора сброшен: изменены данные пары."); bindFieldReuseReset(transitFields, "Контекст предыдущего разбора сброшен: изменены транзитные данные."); Object.values(primaryFields).forEach((field) => { field.addEventListener("input", () => clearTransitDetailsCache()); field.addEventListener("change", () => clearTransitDetailsCache()); }); Object.values(transitFields).forEach((field) => { field.addEventListener("input", () => clearTransitDetailsCache()); field.addEventListener("change", () => clearTransitDetailsCache()); }); bindDateTimeAdvance(primaryFields); bindDateTimeAdvance(personAFields); bindDateTimeAdvance(personBFields); bindDateTimeAdvance(transitFields, "transit_time", "transit_place"); primaryFields.birth_time_quality?.addEventListener("change", applyNatalTimeQualityDefaultLayer); natalChartLayerSelectEl?.addEventListener("change", async () => { state.natalChartLayer = normalizeNatalChartLayer(natalChartLayerSelectEl.value); syncNatalChartLayerUi(); if (isNatalRoute) { const hasRequiredBirthData = Boolean( primaryFields.birth_date?.value && primaryFields.birth_time?.value && primaryFields.birth_place?.value.trim() ); if (!hasRequiredBirthData) { setStatus("Слой карты переключён. Заполните дату, время и место рождения, чтобы построить карту."); return; } setStatus("Слой карты переключён. Перестраиваю карту..."); try { const layer = NATAL_CHART_LAYERS[state.natalChartLayer] || NATAL_CHART_LAYERS.D1; await requestSingleChart(`натал / ${layer.subtitle}`); setStatus("Карта перестроена."); } catch (error) { setStatus( error instanceof Error ? error.message : "Не удалось перестроить карту.", true, ); } } }); bindPrashnaAutofillOptOut(transitFields); transitFields.transit_place?.addEventListener("input", () => { schedulePrashnaPlaceResolve(); }); transitFields.transit_place?.addEventListener("change", () => { schedulePrashnaPlaceResolve(); }); bindFieldRawReset(primaryFields, "primary"); bindSingleCardUpload(transitUploadButtonEl, transitUploadInputEl, null, transitUploadNoteEl, "Транзиты", null, "transit"); bindFieldRawReset(personAFields, "person_a"); bindFieldRawReset(personBFields, "person_b"); bindSingleCardUpload(primaryUploadButtonEl, primaryUploadInputEl, primaryFields, primaryUploadNoteEl, "Основная карта", "primary", "primary"); bindSingleCardUpload(personAUploadButtonEl, personAUploadInputEl, personAFields, personAUploadNoteEl, "Карта мужчины", "primary", "person_a"); bindSingleCardUpload(personBUploadButtonEl, personBUploadInputEl, personBFields, personBUploadNoteEl, "Карта женщины", "secondary", "person_b"); bindPairUpload(); initHeroMeta(); if (assistantReportDateEl && !assistantReportDateEl.value) { assistantReportDateEl.value = new Date().toISOString().slice(0, 10); } updateAssistantPeriodUi(); loadClientProfile(); loadClientReferralProfile(); applyClientProfileToInputFields(); renderHeroQuickNav(); renderClientReferralProfile(); renderTransitsProfileCardPicker(); downloadPdfReportButtonEl?.addEventListener("click", downloadCurrentPdfReport); clientReferralSaveButtonEl?.addEventListener("click", upsertClientReferralProfile); clientReferralCopyButtonEl?.addEventListener("click", copyClientReferralLink); transitsProfileCardSelectEl?.addEventListener("change", () => { state.transitsProfileCardId = transitsProfileCardSelectEl.value || "manual"; state.primaryManualCardForced = state.transitsProfileCardId === "manual"; if (state.transitsProfileCardId !== "manual") { applySelectedTransitsProfileCard(); } else { clearPrimaryManualCardFields(); renderTransitsProfileCardPicker(); } }); primarySelectCardButtonEl?.addEventListener("click", () => { if (!isNatalRoute && !isChildAstrologyRoute && !isTransitsRoute) { return; } state.primaryProfilePickerOpen = !state.primaryProfilePickerOpen; renderTransitsProfileCardPicker(); renderNatalChakraCard(); applyRoutePresentation(); }); personASelectCardButtonEl?.addEventListener("click", () => { if (!isCompatibilityRoute) { return; } state.compatibilityPersonAProfilePickerOpen = !state.compatibilityPersonAProfilePickerOpen; renderCompatibilityProfileCardPicker("a"); }); personBSelectCardButtonEl?.addEventListener("click", () => { if (!isCompatibilityRoute) { return; } state.compatibilityPersonBProfilePickerOpen = !state.compatibilityPersonBProfilePickerOpen; renderCompatibilityProfileCardPicker("b"); }); personAProfileCardSelectEl?.addEventListener("change", () => { state.compatibilityPersonACardId = personAProfileCardSelectEl.value || "manual"; state.compatibilityPersonAManualCardForced = state.compatibilityPersonACardId === "manual"; if (state.compatibilityPersonACardId !== "manual") { void applySelectedCompatibilityProfileCard("a"); } else { clearCompatibilityManualCardFields("a"); renderCompatibilityProfileCardPicker("a"); } }); personBProfileCardSelectEl?.addEventListener("change", () => { state.compatibilityPersonBCardId = personBProfileCardSelectEl.value || "manual"; state.compatibilityPersonBManualCardForced = state.compatibilityPersonBCardId === "manual"; if (state.compatibilityPersonBCardId !== "manual") { void applySelectedCompatibilityProfileCard("b"); } else { clearCompatibilityManualCardFields("b"); renderCompatibilityProfileCardPicker("b"); } }); compatibilityPersonAChartSelectEl?.addEventListener("change", () => { state.compatibilityChartSelections.person_a = normalizeCompatibilityChartLayer(compatibilityPersonAChartSelectEl.value); refreshCompatibilityChartsFromSelector(); }); compatibilityPersonBChartSelectEl?.addEventListener("change", () => { state.compatibilityChartSelections.person_b = normalizeCompatibilityChartLayer(compatibilityPersonBChartSelectEl.value); refreshCompatibilityChartsFromSelector(); }); natalFullAnalysisButtonEl?.addEventListener("click", () => { if (!isNatalRoute && !isChildAstrologyRoute) { return; } if (isNatalRoute) { reachYandexGoal("full_natal_click", { source: "natal_full_analysis_button", }); } state.pendingNatalQuestion = isChildAstrologyRoute ? "Сделайте полный детский разбор карты: темперамент, эмоции, обучение, таланты и зоны поддержки." : "Сделайте полный разбор моей карты."; if (questionEl) { questionEl.value = ""; } state.forceAnalysisFollowup = false; runAnalysisButton?.click(); }); natalCustomQuestionButtonEl?.addEventListener("click", () => { if (!isNatalRoute && !isChildAstrologyRoute) { return; } state.pendingNatalQuestion = ""; state.forceAnalysisFollowup = true; runAnalysisButton?.click(); }); compatibilityFullAnalysisButtonEl?.addEventListener("click", () => { if (!isCompatibilityRoute) { return; } reachYandexGoal("full_compatibility_click", { source: "compatibility_full_analysis_button", }); state.pendingCompatibilityQuestion = compatibilityUsesMoonMode() ? "Сделайте полный лунный разбор совместимости пары по космограмме: эмоциональная динамика, притяжение, напряжения, опоры союза, риски и общий вывод." : "Сделайте полный разбор совместимости пары: динамика отношений, притяжение, напряжения, опоры союза, риски, текущий период и общий вывод."; if (questionEl) { questionEl.value = ""; } state.forceCompatibilityFollowup = false; clearReusableContext(); runAnalysisButton?.click(); }); compatibilityCustomQuestionButtonEl?.addEventListener("click", () => { if (!isCompatibilityRoute) { return; } state.pendingCompatibilityQuestion = ""; state.forceCompatibilityFollowup = true; runAnalysisButton?.click(); }); document.addEventListener("click", (event) => { const pricingButton = event.target.closest("[data-product-pricing-toggle]"); if (!pricingButton) { return; } togglePublicProductPricing(pricingButton); }); transitsSaveCardButtonEl?.addEventListener("click", saveManualTransitsCardToProfile); natalSaveCardButtonEl?.addEventListener("click", saveManualTransitsCardToProfile); personASaveCardButtonEl?.addEventListener("click", () => saveManualCompatibilityCardToProfile("a")); personBSaveCardButtonEl?.addEventListener("click", () => saveManualCompatibilityCardToProfile("b")); personAInlineSaveCardButtonEl?.addEventListener("click", () => saveManualCompatibilityCardToProfile("a")); personBInlineSaveCardButtonEl?.addEventListener("click", () => saveManualCompatibilityCardToProfile("b")); bindBirthPlaceAutocomplete("primary"); bindBirthPlaceAutocomplete("person_a"); bindBirthPlaceAutocomplete("person_b"); [primaryFields.birth_date, primaryFields.birth_time, primaryFields.birth_time_quality, primaryFields.birth_place].forEach((field) => { field?.addEventListener("input", () => { persistClientProfileDraft(); syncTransitDashaButtonState(); if (!isTransitsRoute && !isNatalRoute && !isChildAstrologyRoute) { return; } const selectedCard = getSelectedProfileCard(); const currentCard = buildCurrentPrimaryCard(); if (selectedCard && !areCardDatasEqual(selectedCard, currentCard)) { state.transitsProfileCardId = "manual"; state.primaryManualCardForced = true; if (transitsProfileCardSelectEl) { transitsProfileCardSelectEl.value = "manual"; } } else if (transitsProfileCardSelectEl?.value && transitsProfileCardSelectEl.value !== state.transitsProfileCardId) { state.transitsProfileCardId = transitsProfileCardSelectEl.value; } renderTransitsProfileCardPicker(); }); field?.addEventListener("change", () => { persistClientProfileDraft(); syncTransitDashaButtonState(); }); }); [clientNameEl, clientGenderEl].forEach((field) => { field?.addEventListener("input", () => { persistClientProfileDraft(); }); field?.addEventListener("change", () => { persistClientProfileDraft(); }); }); function handleCompatibilityManualCardChange(target, fields, selectEl, stateKey) { if (!isCompatibilityRoute) { return; } updateCompatibilityTimeModeUi(); const selectedCard = getSelectedCompatibilityProfileCard(target); const currentCard = buildCompatibilityCard(fields); if (selectedCard && !areCardDatasEqual(selectedCard, currentCard)) { state[stateKey] = "manual"; state[target === "a" ? "compatibilityPersonAManualCardForced" : "compatibilityPersonBManualCardForced"] = true; if (selectEl) { selectEl.value = "manual"; } } renderCompatibilityProfileCardPicker(target); } [personAFields.card_name, personAFields.card_gender, personAFields.birth_date, personAFields.birth_time, personAFields.birth_time_quality, personAFields.birth_place].forEach((field) => { const onPersonAChange = () => { if (!isCompatibilityRoute) { return; } handleCompatibilityManualCardChange("a", personAFields, personAProfileCardSelectEl, "compatibilityPersonACardId"); }; field?.addEventListener("input", onPersonAChange); field?.addEventListener("change", onPersonAChange); }); [personBFields.card_name, personBFields.card_gender, personBFields.birth_date, personBFields.birth_time, personBFields.birth_time_quality, personBFields.birth_place].forEach((field) => { const onPersonBChange = () => { if (!isCompatibilityRoute) { return; } handleCompatibilityManualCardChange("b", personBFields, personBProfileCardSelectEl, "compatibilityPersonBCardId"); }; field?.addEventListener("input", onPersonBChange); field?.addEventListener("change", onPersonBChange); }); questionEl?.addEventListener("input", () => { if (questionEl.value.trim()) { state.pendingNatalQuestion = ""; state.pendingCompatibilityQuestion = ""; } }); async function bootstrapClientProfileState() { await refreshClientProfileFromBackend({ reapplyFields: true }); if (state.clientProfile?.user_id) { clientReferralUserIdEl.value = state.clientProfile.user_id; clientReferralNameEl.value = state.clientProfile.client_name || state.clientProfile.display_name || ""; loadClientReferralProfileByUserId(); } else if (state.clientReferral.profile?.user_id) { clientReferralUserIdEl.value = state.clientReferral.profile.user_id; clientReferralNameEl.value = state.clientReferral.profile.display_name || ""; loadClientReferralProfileByUserId(); } } bootstrapClientProfileState(); if (state.admin.available) { loadAdminSession(); updateAdminUi(); adminVerifyButtonEl?.addEventListener("click", verifyAdminSession); adminRefreshButtonEl?.addEventListener("click", () => refreshAdminMetricsSummary()); pricingSaveButtonEl?.addEventListener("click", saveTokenPricing); assistantReportButtonEl?.addEventListener("click", () => generateAssistantReport()); assistantAskButtonEl?.addEventListener("click", askAdminAssistant); assistantRefreshSavedButtonEl?.addEventListener("click", () => refreshSavedAssistantReports()); assistantPeriodPresetEl?.addEventListener("change", updateAssistantPeriodUi); assistantSavedReportsEl?.addEventListener("click", (event) => { const button = event.target.closest("[data-saved-report-date]"); if (!button) { return; } openSavedAssistantReport(button.dataset.savedReportDate); }); referralSaveButtonEl?.addEventListener("click", saveReferralPartner); referralCopyButtonEl?.addEventListener("click", copyReferralLink); referralMarkPaidButtonEl?.addEventListener("click", markReferralPaid); referralPartnerNameInputEl?.addEventListener("input", updateReferralPreview); referralCodeInputEl?.addEventListener("input", updateReferralPreview); referralSourceInputEl?.addEventListener("input", updateReferralPreview); referralCampaignInputEl?.addEventListener("input", updateReferralPreview); referralCardSearchInputEl?.addEventListener("input", (event) => { state.admin.referralCardSearch = event.target.value || ""; renderReferralPartnerCards(state.admin.referralRegistry); }); referralCardSortSelectEl?.addEventListener("change", (event) => { state.admin.referralCardSort = event.target.value || "revenue"; renderReferralPartnerCards(state.admin.referralRegistry); }); adminLogoutButtonEl?.addEventListener("click", () => clearAdminSession("Admin mode disabled.")); adminPasswordEl?.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); verifyAdminSession(); } }); assistantQuestionEl?.addEventListener("keydown", (event) => { if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { event.preventDefault(); askAdminAssistant(); } }); if (state.admin.authenticated) { refreshAdminMetricsSummary({ silent: true }); loadReferralRegistry({ silent: true }); generateAssistantReport({ silent: true }); refreshSavedAssistantReports({ silent: true }); } } updateReferralPreview(); logSiteVisitIfNeeded(); logReferralVisitIfNeeded(); fillExampleButton.addEventListener("click", fillExampleQuestion); supportToggleButtonEl?.addEventListener("click", toggleSupportPanel); supportSubmitEl?.addEventListener("click", askSupportAssistant); supportMessageEl?.addEventListener("keydown", (event) => { if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { event.preventDefault(); askSupportAssistant(); } }); if (!window.PaidVoiceInput) { syncVoiceQuestionButton(); voiceQuestionButtonEl?.addEventListener("click", () => { if (questionSpeechRecording) { stopQuestionSpeechRecognition(); return; } startQuestionSpeechRecognition(); }); } clearHistoryButton.addEventListener("click", () => { state.history = []; state.lastChatDebugContext = null; state.lastChatInputSignature = null; clearAutoNextStep(); renderHistory(); persistWorkspaceCache(); setStatus("История очищена."); }); prashnaRefreshMomentButtonEl?.addEventListener("click", () => { requestHeroGeolocationForMoment(); }); natalChakraTestButtonEl?.addEventListener("click", async () => { await runNatalChakraTest(); }); buildChartButtonEl?.addEventListener("click", async () => { const previousText = buildChartButtonEl.textContent; buildChartButtonEl.disabled = true; buildChartButtonEl.textContent = "Строю карту..."; setStatus("Строю карту без запуска разбора..."); try { if (state.mode === "compatibility") { await requestCompatibilityCharts(); setStatus("Карты пары построены."); } else { await requestSingleChart(state.mode === "transits" ? "карта периода построена вручную" : "карта построена вручную"); setStatus("Карта построена."); } } catch (error) { setStatus( error instanceof Error ? error.message : "Не удалось построить карту.", true, ); } finally { buildChartButtonEl.disabled = false; buildChartButtonEl.textContent = previousText || "Построить карту"; } }); transitDashaButtonEl?.addEventListener("click", async () => { if (normalizeBirthTimeQuality(primaryFields.birth_time_quality?.value || "exact") === "approximate") { syncTransitDashaButtonState(); setTransitDashaStatus("При примерном времени Вимшоттари-тайминг не используется как надёжная основа."); return; } const previousText = transitDashaButtonEl.textContent; transitDashaButtonEl.disabled = true; transitDashaButtonEl.textContent = "Строю даши..."; try { await requestTransitDasha(true); } finally { syncTransitDashaButtonState(); transitDashaButtonEl.textContent = previousText || "Посмотреть даши"; } }); questionEl?.addEventListener("keydown", (event) => { if (event.key !== "Enter" || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey || event.isComposing) { return; } event.preventDefault(); if (!state.requestInFlight) { runAnalysisButton?.click(); } }); runAnalysisButton.addEventListener("click", async () => { await runRequest(debugToggleEl.checked); }); runResponseButton?.addEventListener("click", async () => { await runRequest(false); }); function refreshChartRoot(which, objectEl) { if (!objectEl) { return; } state.chartRoots[which] = objectEl.contentDocument?.documentElement ?? null; if (which === "primary") { const chartBackground = state.chartRoots[which]?.querySelector?.(".chart-bg"); if (chartBackground) { chartBackground.style.fill = state.mode === "prashna" ? "rgba(255, 253, 248, 0.58)" : ""; } } maybeRenderChart(which); } chartPrimaryObjectEl.addEventListener("load", () => { refreshChartRoot("primary", chartPrimaryObjectEl); }); chartSecondaryObjectEl.addEventListener("load", () => { refreshChartRoot("secondary", chartSecondaryObjectEl); }); compatibilityOverlayObjectEl?.addEventListener("load", () => { refreshChartRoot("compatibility_overlay", compatibilityOverlayObjectEl); }); refreshChartRoot("primary", chartPrimaryObjectEl); refreshChartRoot("secondary", chartSecondaryObjectEl); refreshChartRoot("compatibility_overlay", compatibilityOverlayObjectEl); applyRoutePresentation(); updateCompatibilityTimeModeUi(); document.documentElement.classList.add("route-ready"); applyHomeAutoPresentation(); fillExampleQuestion(); updateModeUi(); const restoredWorkspaceCache = restoreWorkspaceCache(); const routePrefill = consumeAutoRoutePrefill(); if ( !restoredWorkspaceCache && routePrefill?.question && ( (routePrefill.route === "natal" && isNatalRoute) || (routePrefill.route === "transits" && isTransitsRoute) || (routePrefill.route === "prashna" && isPrashnaRoute) || (routePrefill.route === "compatibility" && isCompatibilityRoute) ) ) { questionEl.value = routePrefill.question; } if (!restoredWorkspaceCache) { renderHistory(); }