/* korrespond.js — page-scoped UI for /korrespond.php Three-pass flow: Pass 1 may return clarify questions; Pass 2 returns Norwegian + working-language drafts with verified law citations. Pass 3 (opt-in) refines the draft with jurisdiction-scoped formal citations + Rettskilder appendix. */ (function () { 'use strict'; const els = {}; let lang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en'; let uploadFiles = []; let lastClassify = null; let lastFinal = null; let pendingClarifications = {}; let persona = ''; const LANG_LABELS = { en: 'English', no: 'Norsk', uk: 'Українська', pl: 'Polski' }; // All user-facing chrome strings, localized. Drafts themselves stay in NO + working lang. const I18N = { pick_recipient: { en: 'Pick a recipient body before drafting.', no: 'Velg en mottaker før utkast.', pl: 'Wybierz odbiorcę przed sporządzeniem projektu.', uk: 'Виберіть отримувача перед чернеткою.' }, initiate_narrative: { en: 'Describe the situation in "What happened" first.', no: 'Beskriv situasjonen i "Hva skjedde" først.', pl: 'Najpierw opisz sytuację w polu "Co się stało".', uk: 'Спочатку опишіть ситуацію у полі "Що сталося".' }, reply_no_input: { en: 'Reply mode needs the received letter (upload or paste it).', no: 'Svarmodus krever det mottatte brevet (last opp eller lim inn).', pl: 'Tryb odpowiedzi wymaga otrzymanego pisma (prześlij lub wklej).', uk: 'Режим відповіді потребує отриманого листа (завантажте або вставте).' }, analyzing: { en: 'Analyzing…', no: 'Analyserer…', pl: 'Analiza…', uk: 'Аналізую…' }, refining: { en: 'Refining…', no: 'Skriver om…', pl: 'Przepisuję…', uk: 'Переписую…' }, started: { en: 'Started — {body} / {output}', no: 'Startet — {body} / {output}', pl: 'Rozpoczęto — {body} / {output}', uk: 'Розпочато — {body} / {output}' }, fetched_sources: { en: 'Fetched {n} sources for {acts}…', no: 'Hentet {n} kilder for {acts}…', pl: 'Pobrano {n} źródeł dla {acts}…', uk: 'Завантажено {n} джерел для {acts}…' }, need_clarify: { en: 'Need clarification before drafting.', no: 'Trenger avklaring før utkast.', pl: 'Wymaga wyjaśnienia przed sporządzeniem projektu.', uk: 'Потрібне уточнення перед чернеткою.' }, network_error: { en: 'Network error: {msg}', no: 'Nettverksfeil: {msg}', pl: 'Błąd sieci: {msg}', uk: 'Помилка мережі: {msg}' }, stream_error: { en: 'Stream error: {msg}', no: 'Strømfeil: {msg}', pl: 'Błąd strumienia: {msg}', uk: 'Помилка потоку: {msg}' }, request_failed: { en: 'Request failed ({status}).', no: 'Forespørselen mislyktes ({status}).', pl: 'Żądanie nie powiodło się ({status}).', uk: 'Запит не вдалося ({status}).' }, stream_no_draft: { en: 'Stream ended without a draft.', no: 'Strømmen sluttet uten utkast.', pl: 'Strumień zakończył się bez projektu.', uk: 'Потік завершився без чернетки.' }, done_summary: { en: 'Done in {s} s · {n} cited source(s)', no: 'Ferdig på {s} s · {n} sitert(e) kilde(r)', pl: 'Gotowe w {s} s · {n} cytowanych źródeł', uk: 'Готово за {s} с · {n} цитованих джерел' }, refining_status: { en: 'Refining draft with {jur} authorities…', no: 'Skriver om utkast med {jur}-kilder…', pl: 'Przepisuję projekt ze źródłami {jur}…', uk: 'Переписую чернетку з джерелами {jur}…' }, refining_short: { en: 'Refining ({jur})…', no: 'Skriver om ({jur})…', pl: 'Przepisuję ({jur})…', uk: 'Переписую ({jur})…' }, refined_summary: { en: 'Refined in {s} s · {n} cited authority(ies) · {jur}', no: 'Omskrevet på {s} s · {n} sitert(e) rettskilde(r) · {jur}', pl: 'Przepisano w {s} s · {n} cytowanych autorytetów · {jur}', uk: 'Переписано за {s} с · {n} цитованих джерел · {jur}' }, refine_failed: { en: 'Refine failed ({status}).', no: 'Omskriving mislyktes ({status}).', pl: 'Przepisanie nie powiodło się ({status}).', uk: 'Переписування не вдалося ({status}).' }, refine_no_result: { en: 'Refine stream ended without a result.', no: 'Strømmen sluttet uten resultat.', pl: 'Strumień zakończył się bez wyniku.', uk: 'Потік завершився без результату.' }, refine_fetched: { en: 'Fetched {n} authorities for {jur}…', no: 'Hentet {n} rettskilder for {jur}…', pl: 'Pobrano {n} autorytetów dla {jur}…', uk: 'Завантажено {n} джерел для {jur}…' }, working: { en: 'Working…', no: 'Arbeider…', pl: 'Pracuję…', uk: 'Працюю…' }, working_long: { en: 'Pass 1 extracts facts. If anything is missing we will ask. Then Pass 2 runs hard-RAG retrieval, draft, self-check, and translation.', no: 'Pass 1 henter fakta. Hvis noe mangler spør vi. Deretter kjører Pass 2: kilder, utkast, kontroll og oversettelse.', pl: 'Pass 1 wyodrębnia fakty. Jeśli czegoś brak, zapytamy. Następnie Pass 2: źródła, projekt, kontrola, tłumaczenie.', uk: 'Pass 1 витягує факти. Якщо чогось бракує, ми спитаємо. Потім Pass 2: джерела, чернетка, перевірка, переклад.' }, no_cited: { en: 'No cited law sources — draft is plain-language (no § references available from corpus).', no: 'Ingen siterte lovkilder — utkastet er på vanlig språk (ingen §-henvisninger fra korpus).', pl: 'Brak cytowanych źródeł prawa — projekt w zwykłym języku (brak odniesień § z korpusu).', uk: 'Немає цитованих джерел права — чернетка звичайною мовою (без § посилань з корпусу).' }, no_draft_yet: { en: 'No draft to refine. Run a draft first.', no: 'Ingen utkast å skrive om. Lag et utkast først.', pl: 'Brak projektu do przepisania. Najpierw stwórz projekt.', uk: 'Немає чернетки для переписування. Спочатку створіть чернетку.' }, // Section/header chrome ready_title: { en: 'Ready', no: 'Klar', pl: 'Gotowe', uk: 'Готовий' }, ready_desc: { en: 'Pick a recipient body, describe the situation, choose an output type and tone, then run. Drafts always come back in Norwegian bokmål + your working language, side-by-side, with verified law citations.', no: 'Velg en mottaker, beskriv situasjonen, velg utdataformat og tone, og kjør. Utkast leveres alltid på norsk bokmål + arbeidsspråket, side ved side, med verifiserte lovhenvisninger.', pl: 'Wybierz odbiorcę, opisz sytuację, wybierz format i ton, uruchom. Projekty zawsze zwracane są po norwesku (bokmål) + w Twoim języku, obok siebie, ze zweryfikowanymi cytatami prawa.', uk: 'Виберіть отримувача, опишіть ситуацію, виберіть формат і тон, запустіть. Чернетки повертаються норвезькою (bokmål) + вашою мовою, поряд, з перевіреними посиланнями на закон.' }, summary_pass1: { en: 'Summary (Pass 1)', no: 'Sammendrag (Pass 1)', pl: 'Podsumowanie (Pass 1)', uk: 'Резюме (Pass 1)' }, assumed_legal: { en: 'Presumed legal basis:', no: 'Antatt rettslig grunnlag:', pl: 'Domniemana podstawa prawna:', uk: 'Передбачувана правова основа:' }, deadlines_label: { en: 'Deadlines:', no: 'Frister:', pl: 'Terminy:', uk: 'Терміни:' }, canonical: { en: 'canonical', no: 'kanonisk', pl: 'kanoniczny', uk: 'канонічний' }, reference: { en: 'reference', no: 'referanse', pl: 'referencja', uk: 'довідка' }, refined_label: { en: 'refined', no: 'omskrevet', pl: 'przepisany', uk: 'переписаний' }, copy: { en: 'Copy', no: 'Kopier', pl: 'Kopiuj', uk: 'Копіювати' }, copied: { en: 'Copied ✓', no: 'Kopiert ✓', pl: 'Skopiowano ✓', uk: 'Скопійовано ✓' }, download_txt: { en: 'Download .txt', no: 'Last ned .txt', pl: 'Pobierz .txt', uk: 'Завантажити .txt' }, cited_law_n: { en: 'Cited law ({n})', no: 'Siterte kilder ({n})', pl: 'Cytowane prawo ({n})', uk: 'Цитоване право ({n})' }, cited_law_hint: { en: 'each reference traces to a real corpus passage', no: 'hver henvisning kan spores til en reell korpuspassasje', pl: 'każde odniesienie pochodzi z prawdziwego fragmentu korpusu', uk: 'кожне посилання простежується до реального уривка корпусу' }, cited_authorities_n:{ en: 'Cited authorities ({n})', no: 'Siterte rettskilder ({n})', pl: 'Cytowane autorytety ({n})', uk: 'Цитовані джерела ({n})' }, view_source: { en: 'View source', no: 'Se kilde', pl: 'Zobacz źródło', uk: 'Переглянути джерело' }, flag_cites: { en: 'Citations verified', no: 'Henvisninger verifisert', pl: 'Cytaty zweryfikowane', uk: 'Цитати перевірено' }, flag_deadline: { en: 'Deadline', no: 'Frist', pl: 'Termin', uk: 'Термін' }, flag_goal: { en: 'Goal addressed', no: 'Mål adressert', pl: 'Cel uwzględniony', uk: 'Мета врахована' }, flag_tone: { en: 'Tone', no: 'Tone', pl: 'Ton', uk: 'Тон' }, refine_title: { en: 'Refine with formal citations', no: 'Skriv om med formelle henvisninger', pl: 'Przepisz z formalnymi cytatami', uk: 'Переписати з формальними цитатами' }, one_extra_credit: { en: '(+1 credit)', no: '(+1 kreditt)', pl: '(+1 kredyt)', uk: '(+1 кредит)' }, refine_hint: { en: 'Optional 2nd pass: pull fresh authorities, rewrite citations in formal style ("jf. forvaltningsloven § 17", "jf. Strand Lobben m.fl. mot Norge, EMD-37283/13, §§ 207–214"), and append a Rettskilder block at the bottom.', no: 'Valgfri 2. omgang: hent ferske rettskilder, skriv om henvisningene formelt ("jf. forvaltningsloven § 17", "jf. Strand Lobben m.fl. mot Norge, EMD-37283/13, §§ 207–214"), og legg ved en Rettskilder-blokk nederst.', pl: 'Opcjonalna 2. tura: pobierz świeże autorytety, przepisz cytaty w stylu formalnym, dołącz blok Rettskilder na końcu.', uk: 'Опційний 2-й прохід: завантажити свіжі джерела, переписати цитати у формальному стилі, додати блок Rettskilder в кінці.' }, jur_norwegian: { en: 'Norwegian law only', no: 'Kun norsk rett', pl: 'Tylko prawo norweskie', uk: 'Лише норвезьке право' }, jur_echr: { en: 'ECHR (EMK + HUDOC)', no: 'EMK (EMK + HUDOC)', pl: 'EKPC (EMK + HUDOC)', uk: 'ЄКПЛ (EMK + HUDOC)' }, jur_both: { en: 'Both', no: 'Begge', pl: 'Oba', uk: 'Обидва' }, refine_btn: { en: 'Refine citations', no: 'Skriv om henvisninger', pl: 'Przepisz cytaty', uk: 'Переписати цитати' }, refine_btn_busy: { en: 'Refining…', no: 'Skriver om…', pl: 'Przepisuję…', uk: 'Переписую…' }, // Clarify chrome clarify_title: { en: 'Before we draft, clarify:', no: 'Før vi skriver, avklar:', pl: 'Zanim sporządzimy projekt, wyjaśnij:', uk: 'Перш ніж писати, уточніть:' }, clarify_hint: { en: 'Answer what you can, then click Continue draft. Or click Draft anyway to proceed with what we have.', no: 'Svar på det du kan, og klikk Fortsett utkast. Eller klikk Skriv likevel for å fortsette med det vi har.', pl: 'Odpowiedz, co możesz, i kliknij Kontynuuj projekt. Albo kliknij Mimo to napisz, aby kontynuować z tym, co mamy.', uk: 'Дайте відповідь, що можете, потім натисніть Продовжити. Або натисніть Все одно написати, щоб продовжити з тим, що є.' }, clarify_continue: { en: 'Continue draft', no: 'Fortsett utkast', pl: 'Kontynuuj projekt', uk: 'Продовжити' }, clarify_force: { en: 'Draft anyway', no: 'Skriv likevel', pl: 'Mimo to napisz', uk: 'Все одно написати' }, optional: { en: '(optional)', no: '(valgfritt)', pl: '(opcjonalnie)', uk: '(необов’язково)' }, // Bracket label inside drafts column header — value-only (the language NAME stays its own name) no_column: { en: 'Norsk (bokmål)', no: 'Norsk (bokmål)', pl: 'Norsk (bokmål)', uk: 'Norsk (bokmål)' }, }; function t(key, vars) { const m = I18N[key]; if (!m) return key; let s = m[lang] || m.en || key; if (vars) for (const k in vars) s = s.replaceAll('{' + k + '}', String(vars[k])); return s; } document.addEventListener('DOMContentLoaded', () => { if (document.body.dataset.activeTool !== 'korrespond') return; Object.assign(els, { form: document.getElementById('korrForm'), status: document.getElementById('korrStatus'), runButton: document.getElementById('korrRunButton'), results: document.getElementById('korrResults'), langButtons: Array.from(document.querySelectorAll('#korrLangSwitcher .lang-btn')), modeRadios: Array.from(document.querySelectorAll('input[name="korrMode"]')), bodySelect: document.getElementById('korrBody'), outputRadios: Array.from(document.querySelectorAll('input[name="korrOutput"]')), toneRadios: Array.from(document.querySelectorAll('input[name="korrTone"]')), caseRef: document.getElementById('korrCaseRef'), where: document.getElementById('korrWhere'), deadline: document.getElementById('korrDeadline'), parties: document.getElementById('korrParties'), narrative: document.getElementById('korrNarrative'), goal: document.getElementById('korrGoal'), goalChips: Array.from(document.querySelectorAll('#korrGoalChips .korr-chip')), uploadZone: document.getElementById('korrUploadZone'), uploadInput: document.getElementById('korrUploadInput'), uploadPrompt: document.getElementById('korrUploadPrompt'), uploadFileInfo: document.getElementById('korrUploadFileInfo'), uploadFileList: document.getElementById('korrUploadFileList'), uploadClear: document.getElementById('korrUploadClear'), clarifyPanel: document.getElementById('korrClarifyPanel'), clarifyList: document.getElementById('korrClarifyList'), clarifyContinue:document.getElementById('korrClarifyContinue'), clarifyForce: document.getElementById('korrClarifyForce'), personaControl: document.getElementById('korrPersonaControl'), personaSelect: document.getElementById('korrPersonaSelect'), }); if (!els.form) return; bindLang(); bindGoalChips(); bindUpload(); bindClarify(); loadPersonas(); applyStaticI18n(); els.form.addEventListener('submit', (e) => { e.preventDefault(); runRequest(false); }); }); // Apply [data-i18n] attributes on static DOM (and re-apply after lang switch). function applyStaticI18n() { document.querySelectorAll('[data-i18n]').forEach((el) => { const key = el.getAttribute('data-i18n'); if (key) el.textContent = t(key); }); } // ── Language switcher ─────────────────────────────────────────────────────── function bindLang() { els.langButtons.forEach((b) => { b.classList.toggle('is-active', b.dataset.lang === lang); b.addEventListener('click', () => { els.langButtons.forEach((x) => x.classList.remove('is-active')); b.classList.add('is-active'); lang = b.dataset.lang || 'en'; localStorage.setItem('dbn-ui-lang', lang); applyStaticI18n(); }); }); } function bindGoalChips() { els.goalChips.forEach((chip) => { chip.addEventListener('click', () => { els.goal.value = chip.dataset.goal || ''; els.goalChips.forEach((c) => c.classList.remove('is-active')); chip.classList.add('is-active'); }); }); } // ── File upload ───────────────────────────────────────────────────────────── function bindUpload() { if (!els.uploadZone) return; const onFiles = (fileList) => { const files = Array.from(fileList || []).slice(0, 4); if (uploadFiles.length + files.length > 4) { setStatus('At most 4 files can be uploaded per request.', 'error'); return; } files.forEach((f) => { if (f.size > 8 * 1024 * 1024) { setStatus(`${f.name} exceeds the 8 MB limit.`, 'error'); return; } const ext = (f.name.split('.').pop() || '').toLowerCase(); if (!['pdf', 'docx', 'txt'].includes(ext)) { setStatus(`${f.name} is not a supported file type (PDF, DOCX, TXT).`, 'error'); return; } uploadFiles.push(f); }); renderUploadList(); }; els.uploadInput.addEventListener('change', (e) => onFiles(e.target.files)); els.uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); els.uploadZone.classList.add('is-drop'); }); els.uploadZone.addEventListener('dragleave', () => els.uploadZone.classList.remove('is-drop')); els.uploadZone.addEventListener('drop', (e) => { e.preventDefault(); els.uploadZone.classList.remove('is-drop'); onFiles(e.dataTransfer?.files); }); els.uploadClear?.addEventListener('click', () => { uploadFiles = []; els.uploadInput.value = ''; renderUploadList(); }); } function renderUploadList() { if (!uploadFiles.length) { els.uploadFileInfo.classList.add('is-hidden'); els.uploadPrompt.classList.remove('is-hidden'); return; } els.uploadPrompt.classList.add('is-hidden'); els.uploadFileInfo.classList.remove('is-hidden'); els.uploadFileList.innerHTML = uploadFiles.map((f) => { const kb = (f.size / 1024).toFixed(0); return `
${esc(t('working_long'))}
${esc(c.summary)}
${c.applicable_acts && c.applicable_acts.length ? `${esc(t('assumed_legal'))} ${c.applicable_acts.map(esc).join(', ')}
` : ''} ${c.deadlines && c.deadlines.length ? `${esc(t('deadlines_label'))} ${c.deadlines.map(esc).join(', ')}
` : ''} `; } function renderFinal(data) { const userLang = data.draft_user_lang || 'en'; const userLangLabel = LANG_LABELS[userLang] || userLang.toUpperCase(); const flags = data.self_check || {}; const cited = data.cited_law || []; const flagBadge = (key, label) => { const v = flags[key] || 'ok'; const cls = v === 'ok' ? 'is-ok' : (v === 'warn' ? 'is-warn' : 'is-error'); const icon = v === 'ok' ? '✓' : '!'; return `${icon} ${esc(label)}`; }; const draftNo = data.draft_no || ''; const draftUser = data.draft_user || ''; const isSameLang = userLang === 'no'; els.results.innerHTML = `${esc(draftNo)}
${esc(draftUser)}
${esc(s.excerpt || '')}
${s.source_url ? `${esc(t('view_source'))}` : ''}${esc(t('no_cited'))}
`} ${data.disclaimer ? `${esc(data.disclaimer)}
` : ''} ${(data.legal_check && data.legal_check.length) ? `${esc(f.what_to_check)}
${esc(t('refine_hint'))}
${esc(draftNo)}
${esc(draftUser)}
${esc(s.excerpt || '')}
${s.source_url ? `${esc(t('view_source'))}` : ''}${esc(f.what_to_check)}