<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AssemblyLine DOCX Labeler</title>
    <link rel="icon" type="image/png" href="/packagestatic/docassemble.ALDashboard/litlabtheme/dal-favicon.png">
    <link rel="stylesheet" href="/packagestatic/docassemble.ALDashboard/litlabtheme/bootstrap-theme.css">
    <link rel="stylesheet" href="/packagestatic/docassemble.ALDashboard/litlabtheme/variables.css">
    <link rel="stylesheet" href="/packagestatic/docassemble.ALDashboard/litlabtheme/custom.css">
    <link rel="stylesheet" href="/packagestatic/docassemble.ALDashboard/docx_labeler.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js"></script>
</head>
<body>
    <div id="app" class="dl-app">
        <!-- Header -->
        <header class="dl-header navbar bg-dark px-4 py-3">
            <div class="container-fluid d-flex align-items-center justify-content-between">
                <div class="d-flex align-items-center gap-3">
                    <img src="/packagestatic/docassemble.ALDashboard/litlabtheme/dal-favicon.png" alt="LIT Lab logo" width="28" height="28">
                    <div class="al-title">
                        <h1 class="h4 fw-bold mb-0 text-white">AssemblyLine DOCX Labeler</h1>
                        <p class="small mt-1 mb-0 text-white">Add or edit Jinja2 template variables</p>
                    </div>
                </div>
                <div class="d-flex align-items-center gap-3">
                    <div id="auth-controls" class="position-relative"></div>
                    <button id="utilities-btn" class="btn btn-outline-light btn-sm">
                        Utilities
                    </button>
                    <button id="repair-btn" class="btn btn-outline-light btn-sm">
                        Repair
                    </button>
                    <button id="settings-btn" class="btn btn-outline-light btn-sm">
                        <svg class="me-1" width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
                        </svg>
                        Settings
                    </button>
                </div>
            </div>
        </header>

        <!-- Main Content -->
        <main class="dl-main">
            <!-- Sidebar -->
            <aside id="sidebar-panel" class="dl-sidebar">
                <div class="dl-upload-section">
                    <label class="d-block">
                        <span class="visually-hidden">Choose DOCX file</span>
                        <input type="file" id="file-input" accept=".docx" class="dl-file-input">
                    </label>
                    <button id="open-playground-btn" class="btn btn-sm btn-outline-secondary mt-2 hidden" title="Open a DOCX from the Playground templates folder">
                        <svg style="width:14px;height:14px;" class="me-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z"></path></svg>
                        Open from Playground
                    </button>
                    <p id="file-name" class="mt-2 small text-muted hidden"></p>
                    <div id="ai-auth-notice" class="alert alert-warning small mt-2 mb-0 hidden"></div>
                    <div id="interview-picker-sidebar-host" class="hidden mt-3">
                        <div id="interview-picker-panel">
                            <div class="small fw-medium mb-2">Interview variables source</div>
                            <div class="btn-group btn-group-sm w-100 mb-2" role="group" aria-label="Interview source mode">
                                <input type="radio" class="btn-check" name="interview-source-mode" id="source-mode-playground" value="playground" checked>
                                <label class="btn btn-outline-secondary" for="source-mode-playground">Playground</label>
                                <input type="radio" class="btn-check" name="interview-source-mode" id="source-mode-installed" value="installed">
                                <label class="btn btn-outline-secondary" for="source-mode-installed">Installed</label>
                            </div>
                            <div id="playground-fields">
                                <div class="mb-2">
                                    <label class="form-label small mb-1" for="playground-project">Project</label>
                                    <select id="playground-project" class="form-select form-select-sm"></select>
                                </div>
                                <div>
                                    <label class="form-label small mb-1" for="playground-yaml-file">YAML file</label>
                                    <select id="playground-yaml-file" class="form-select form-select-sm"></select>
                                </div>
                            </div>
                            <div id="installed-fields" class="hidden">
                                <div class="mb-2">
                                    <label class="form-label small mb-1" for="installed-package">Package</label>
                                    <select id="installed-package" class="form-select form-select-sm"></select>
                                </div>
                                <div>
                                    <label class="form-label small mb-1" for="installed-yaml-file">YAML file</label>
                                    <select id="installed-yaml-file" class="form-select form-select-sm"></select>
                                </div>
                            </div>
                            <p id="interview-variable-summary" class="small text-muted mt-2 mb-0 hidden"></p>
                        </div>
                    </div>
                </div>

                <!-- Loading State -->
                <div id="loading-state" class="hidden p-5 text-center">
                    <svg class="loading-spinner dl-state-icon mx-auto text-primary" fill="none" viewBox="0 0 24 24">
                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                    </svg>
                    <p id="loading-message" class="mt-3 text-muted">Processing document...</p>
                </div>

                <!-- Error State -->
                <div id="error-state" class="hidden alert alert-danger m-3">
                    <p id="error-message" class="mb-0 small"></p>
                </div>

                <!-- Empty State -->
                <div id="empty-state" class="flex-fill d-flex align-items-center justify-content-center p-4 text-center">
                    <div>
                        <svg class="dl-state-icon-lg mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
                        </svg>
                        <p class="mt-3 text-muted">Upload a DOCX file to get started</p>
                    </div>
                </div>

                <!-- Main Panel (tabs for existing labels vs suggestions) -->
                <div id="main-panel" class="hidden flex-fill d-flex flex-column overflow-hidden">
                    <!-- Tabs -->
                    <div class="dl-tabs">
                        <button id="tab-existing" class="dl-tab tab-active">
                            Existing Labels <span id="existing-count" class="dl-count-badge dl-count-badge-primary">0</span>
                        </button>
                        <button id="tab-manual" class="dl-tab hidden">
                            Manual Labels <span id="manual-count" class="dl-count-badge dl-count-badge-warning">0</span>
                        </button>
                        <button id="tab-suggestions" class="dl-tab">
                            AI Suggestions <span id="suggestions-count" class="dl-count-badge dl-count-badge-warning">0</span>
                        </button>
                    </div>

                    <!-- Existing Labels Panel -->
                    <div id="existing-panel" class="flex-fill d-flex flex-column overflow-hidden">
                        <div class="dl-panel-toolbar d-flex align-items-center gap-2">
                            <button id="bulk-replace-btn" class="btn btn-sm btn-outline-primary">Bulk Replace</button>
                            <button id="ai-relabel-btn" class="btn btn-sm btn-primary">AI Relabel All</button>
                        </div>
                        <div id="existing-labels-tree" class="flex-fill overflow-auto p-3"></div>
                    </div>

                    <!-- Manual Labels Panel -->
                    <div id="manual-panel" class="hidden flex-fill d-flex flex-column overflow-hidden">
                        <div class="dl-panel-toolbar d-flex align-items-center justify-content-between gap-2">
                            <div class="small text-muted">Labels added from preview selections</div>
                        </div>
                        <div id="manual-labels-list" class="flex-fill overflow-auto p-3"></div>
                    </div>

                    <!-- Suggestions Panel -->
                    <div id="suggestions-panel" class="hidden flex-fill d-flex flex-column overflow-hidden">
                        <div class="dl-panel-toolbar d-flex align-items-center justify-content-between">
                            <div class="d-flex gap-2">
                                <button id="accept-all-btn" class="btn btn-sm btn-success">Accept All</button>
                                <button id="regenerate-btn" class="btn btn-sm btn-outline-secondary">Regenerate</button>
                            </div>
                            <div class="form-check small mb-0">
                                <input class="form-check-input" type="checkbox" id="toggle-low-confidence">
                                <label class="form-check-label" for="toggle-low-confidence">Show low confidence</label>
                            </div>
                        </div>
                        <div id="low-confidence-summary" class="small text-muted px-3 pt-2"></div>
                        <div id="suggestions-validation" class="hidden mx-3 mt-2 alert alert-warning small"></div>
                        <div id="suggestions-list" class="flex-fill overflow-auto p-3"></div>
                    </div>

                    <!-- Download Button -->
                    <div class="dl-download-bar">
                        <div id="syntax-validation" class="hidden alert small mb-2"></div>
                        <button id="apply-highlights-btn" class="btn btn-outline-secondary w-100 py-2 fw-semibold mb-2" disabled>
                            Apply Highlights
                        </button>
                        <button id="download-btn" class="btn btn-primary w-100 py-2 fw-semibold" disabled>
                            Download Updated DOCX
                        </button>
                        <button id="save-playground-btn" class="btn btn-outline-success w-100 py-2 fw-semibold mt-2 hidden" disabled title="Save this DOCX back to the Playground templates folder">
                            <svg style="width:14px;height:14px;" class="me-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path></svg>
                            Save to Playground
                        </button>
                        <p id="download-status" class="mt-2 small text-muted text-center hidden"></p>
                    </div>
                </div>
            </aside>

            <!-- Document Preview -->
            <div class="dl-preview-area">
                <div id="preview-empty" class="h-100 d-flex align-items-center justify-content-center text-center">
                    <div>
                        <svg class="dl-state-icon-xl mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
                        </svg>
                        <h3 class="mt-3 h5 text-muted">Document Preview</h3>
                        <p class="mt-2 text-muted">Your document will appear here</p>
                    </div>
                </div>
                <div id="preview-content" class="hidden dl-preview-card document-preview"></div>
            </div>
        </main>

        <!-- Settings Modal -->
        <div id="settings-modal" class="hidden dl-modal-overlay">
            <div class="dl-modal dl-modal-md">
                <div class="dl-modal-header">
                    <h2 class="h5 fw-semibold mb-0">AI Prompt Settings</h2>
                    <button id="close-settings" class="btn btn-sm btn-light rounded">
                        <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
                        </svg>
                    </button>
                </div>
                <div class="dl-modal-body" style="max-height:60vh;overflow-y:auto">
                    <div class="mb-4">
                        <label class="form-label fw-medium" for="additional-instructions">Additional Instructions</label>
                        <textarea id="additional-instructions" rows="4" placeholder="Add any specific instructions for the AI labeling process..." class="form-control"></textarea>
                    </div>
                    <div class="mb-4">
                        <label class="form-label fw-medium" for="context-text">Add Context (Optional)</label>
                        <textarea id="context-text" rows="5" placeholder="Paste any optional background context that helps explain the template, source material, or domain." class="form-control"></textarea>
                        <div class="form-text">Optional context for the AI prompt.</div>
                    </div>
                    <div class="mb-4">
                        <label class="form-label fw-medium" for="custom-people">Custom People Names</label>
                        <textarea id="custom-people" rows="3" placeholder='[["clients", "the people benefiting from the form"]]' class="form-control font-monospace small"></textarea>
                    </div>
                    <div class="mb-4">
                        <label class="form-label fw-medium" for="prompt-profile">Prompt Profile</label>
                        <select id="prompt-profile" class="form-select">
                            <option value="standard" selected>General forms</option>
                            <option value="litigation_template">Litigation / pleading templates</option>
                        </select>
                        <div class="form-text">Use the litigation profile for pleadings with captions, bracketed drafting notes, and many visible blanks.</div>
                    </div>
                    <div class="mb-4">
                        <label class="form-label fw-medium" for="generation-method">Suggestion Generation Method</label>
                        <select id="generation-method" class="form-select">
                            <option value="single">Single model, single run</option>
                            <option value="multi_run" selected>Merge multiple runs (3 fixed runs)</option>
                            <option value="multi_model">Multi-model ensemble</option>
                        </select>
                        <div class="form-text">Single pass, multiple runs of one model, or multi-model ensemble with judge review.</div>
                    </div>
                    <div id="generator-models-group" class="mb-4 hidden">
                        <label class="form-label fw-medium" for="generator-models">Generator Models</label>
                        <textarea id="generator-models" rows="3" placeholder="One model per line, or comma-separated" class="form-control font-monospace small"></textarea>
                        <div class="form-text">Recommended for ensemble mode: 2-3 models.</div>
                    </div>
                    <div class="mb-4">
                        <label class="form-label fw-medium" for="judge-model">Judge Model</label>
                        <input id="judge-model" type="text" class="form-control font-monospace small" placeholder="Leave blank to use the selected model">
                        <div class="form-text">Used when candidates conflict or only low-confidence options are available.</div>
                    </div>
                    <div class="form-check mb-4">
                        <input class="form-check-input" type="checkbox" id="defragment-runs" checked>
                        <label class="form-check-label fw-medium" for="defragment-runs">
                            Consolidate fragmented runs before labeling and download
                        </label>
                        <div class="form-text">Recommended when placeholder text is split across multiple runs.</div>
                    </div>
                    <div id="use-playground-vars-group" class="mb-4 hidden">
                        <div class="d-flex align-items-start justify-content-between gap-3">
                            <div class="form-check flex-grow-1 mb-0">
                                <input class="form-check-input" type="checkbox" id="use-playground-variables">
                                <label class="form-check-label fw-medium" for="use-playground-variables">
                                    Prefer variables from selected interview
                                </label>
                                <div id="playground-settings-summary" class="form-text">
                                    Select a Playground or installed interview from the sidebar to use its variable list in the prompt.
                                </div>
                            </div>
                            <button type="button" id="change-interview-source-btn" class="btn btn-sm btn-outline-secondary hidden">
                                Change
                            </button>
                        </div>
                        <div id="settings-interview-picker-host" class="hidden mt-3"></div>
                    </div>
                    <div>
                        <label class="form-label fw-medium" for="ai-model">AI Model</label>
                        <input id="ai-model" type="text" class="form-control font-monospace small" placeholder="Type to find a model">
                        <p class="form-text mb-1">Type to filter available models.</p>
                        <div id="ai-model-suggestions" class="d-flex flex-wrap gap-1"></div>
                    </div>
                </div>
                <div class="dl-modal-footer d-flex justify-content-end gap-2">
                    <button id="reset-settings" class="btn btn-outline-secondary">Reset to Default</button>
                    <button id="save-settings" class="btn btn-primary">Save Settings</button>
                </div>
            </div>
        </div>

        <!-- Bulk Replace Modal -->
        <div id="bulk-replace-modal" class="hidden dl-modal-overlay">
            <div class="dl-modal dl-modal-sm">
                <div class="dl-modal-header">
                    <div>
                        <h2 class="h5 fw-semibold mb-1">Bulk Find &amp; Replace</h2>
                        <p class="text-muted small mb-0">Replace variable names across all labels</p>
                    </div>
                </div>
                <div class="dl-modal-body">
                    <div class="mb-3">
                        <label class="form-label fw-medium small">Find (regex supported)</label>
                        <input type="text" id="bulk-find" placeholder="client|plaintiff" class="form-control font-monospace">
                    </div>
                    <div class="mb-3">
                        <label class="form-label fw-medium small">Replace with</label>
                        <input type="text" id="bulk-replace" placeholder="users[0]" class="form-control font-monospace">
                    </div>
                    <div id="bulk-preview" class="hidden p-3 bg-light rounded" style="max-height:12rem;overflow-y:auto">
                        <p class="fw-medium small text-muted mb-2">Preview</p>
                        <div id="bulk-preview-list" class="small"></div>
                    </div>
                </div>
                <div class="dl-modal-footer d-flex justify-content-between">
                    <button id="bulk-preview-btn" class="btn btn-outline-primary btn-sm">Preview Changes</button>
                    <div class="d-flex gap-2">
                        <button id="bulk-cancel" class="btn btn-outline-secondary btn-sm">Cancel</button>
                        <button id="bulk-apply" class="btn btn-primary btn-sm">Apply</button>
                    </div>
                </div>
            </div>
        </div>

        <!-- Create/Edit Label Modal -->
        <div id="edit-label-modal" class="hidden dl-modal-overlay">
            <div class="dl-modal dl-modal-lg">
                <div class="dl-modal-header">
                    <h2 id="edit-label-title" class="h5 fw-semibold mb-0">Edit Label</h2>
                    <button id="edit-close" class="btn btn-sm btn-light rounded">
                        <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
                        </svg>
                    </button>
                </div>
                <div class="flex-fill d-flex overflow-hidden">
                    <!-- Left: Input area -->
                    <div class="w-50 p-3 border-end d-flex flex-column">
                        <div class="mb-3">
                            <label class="form-label fw-medium small">Current Value</label>
                            <p id="edit-label-original" class="px-3 py-2 bg-light rounded font-monospace small break-all mb-0"></p>
                        </div>
                        <div class="mb-3">
                            <label class="form-label fw-medium small">New Variable Name</label>
                            <input type="text" id="edit-label-input" placeholder="{{ users[0].name.first }}" class="form-control font-monospace">
                            <p class="form-text small">Click a variable on the right to insert it</p>
                        </div>
                    </div>
                    <!-- Right: Variable picker tree -->
                    <div class="w-50 d-flex flex-column bg-light">
                        <div class="p-2 border-bottom">
                            <input type="text" id="variable-search" placeholder="Search variables..." class="form-control form-control-sm">
                        </div>
                        <div id="variable-tree" class="flex-fill overflow-auto p-2 small"></div>
                    </div>
                </div>
                <div class="dl-modal-footer d-flex justify-content-end gap-2">
                    <button id="edit-cancel" class="btn btn-outline-secondary">Cancel</button>
                    <button id="edit-save" class="btn btn-primary">Save</button>
                </div>
            </div>
        </div>

        <!-- Open from Playground modal -->
        <div id="open-playground-modal" class="hidden position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center" style="background: rgba(0,0,0,0.5); z-index: 1070;">
            <div class="bg-white border shadow p-3" style="width: 100%; max-width: 30rem;">
                <div class="d-flex align-items-center justify-content-between border-bottom pb-2 mb-3">
                    <h2 class="h5 fw-semibold mb-0">Open from Playground</h2>
                    <button id="close-open-playground" class="btn btn-sm btn-light">Close</button>
                </div>
                <div class="mb-2">
                    <label class="form-label small mb-1" for="open-pg-project">Project</label>
                    <select id="open-pg-project" class="form-select form-select-sm"></select>
                </div>
                <div class="mb-3">
                    <label class="form-label small mb-1" for="open-pg-template">Template file</label>
                    <select id="open-pg-template" class="form-select form-select-sm"></select>
                </div>
                <div class="d-flex justify-content-end gap-2">
                    <button id="open-pg-cancel" class="btn btn-outline-secondary btn-sm">Cancel</button>
                    <button id="open-pg-confirm" class="btn btn-primary btn-sm">Open</button>
                </div>
            </div>
        </div>

        <!-- Save to Playground modal -->
        <div id="save-playground-modal" class="hidden position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center" style="background: rgba(0,0,0,0.5); z-index: 1070;">
            <div class="bg-white border shadow p-3" style="width: 100%; max-width: 30rem;">
                <div class="d-flex align-items-center justify-content-between border-bottom pb-2 mb-3">
                    <h2 class="h5 fw-semibold mb-0">Save to Playground</h2>
                    <button id="close-save-playground" class="btn btn-sm btn-light">Close</button>
                </div>
                <div class="mb-2">
                    <label class="form-label small mb-1" for="save-pg-project">Project</label>
                    <select id="save-pg-project" class="form-select form-select-sm"></select>
                </div>
                <div class="mb-3">
                    <label class="form-label small mb-1" for="save-pg-filename">Filename</label>
                    <input id="save-pg-filename" type="text" class="form-control form-control-sm" placeholder="template.docx">
                </div>
                <p id="save-pg-status" class="small text-muted hidden"></p>
                <div class="d-flex justify-content-end gap-2">
                    <button id="save-pg-cancel" class="btn btn-outline-secondary btn-sm">Cancel</button>
                    <button id="save-pg-confirm" class="btn btn-primary btn-sm">Save</button>
                </div>
            </div>
        </div>

        <!-- Utilities Modal -->
        <div id="utilities-modal" class="hidden dl-modal-overlay">
            <div class="dl-modal dl-modal-lg">
                <div class="dl-modal-header">
                    <div>
                        <h2 class="h5 fw-semibold mb-1">Utilities</h2>
                        <p class="text-muted small mb-0">Run DOCX utilities on the active file or a separate upload.</p>
                    </div>
                    <button id="utilities-close" class="btn btn-sm btn-light rounded">Close</button>
                </div>
                <div class="dl-modal-body" style="max-height:65vh;overflow-y:auto">
                    <div class="mb-3">
                        <div class="form-check mb-2">
                            <input class="form-check-input" type="radio" name="utility-source" id="utility-source-active" checked>
                            <label class="form-check-label" for="utility-source-active">Use active DOCX</label>
                        </div>
                        <div class="form-check mb-2">
                            <input class="form-check-input" type="radio" name="utility-source" id="utility-source-upload">
                            <label class="form-check-label" for="utility-source-upload">Use a separate DOCX upload</label>
                        </div>
                        <input type="file" id="utility-file-input" accept=".docx" class="form-control form-control-sm hidden">
                        <div id="utility-source-status" class="form-text">The active DOCX will be used when available.</div>
                    </div>
                    <div class="d-grid gap-2">
                        <button class="btn btn-outline-primary utility-action-btn" data-action="validate">Validate</button>
                        <button class="btn btn-outline-primary utility-action-btn" data-action="schema-validate">Validate with OOXML Schema</button>
                        <button class="btn btn-outline-primary utility-action-btn" data-action="defragment-runs">Merge / Defragment Runs</button>
                        <button class="btn btn-outline-primary utility-action-btn" data-action="cleanup-docx">Cleanup DOCX</button>
                        <button class="btn btn-outline-primary utility-action-btn" data-action="highlight-jinja2">Highlight Jinja2</button>
                    </div>
                    <div id="utilities-result" class="mt-3 small"></div>
                </div>
            </div>
        </div>

        <!-- Repair Modal -->
        <div id="repair-modal" class="hidden dl-modal-overlay">
            <div class="dl-modal dl-modal-lg">
                <div class="dl-modal-header">
                    <div>
                        <h2 class="h5 fw-semibold mb-1">Repair</h2>
                        <p class="text-muted small mb-0">Try increasingly aggressive rescue steps for broken DOCX files.</p>
                    </div>
                    <button id="repair-close" class="btn btn-sm btn-light rounded">Close</button>
                </div>
                <div class="dl-modal-body" style="max-height:65vh;overflow-y:auto">
                    <div class="mb-3">
                        <div class="form-check mb-2">
                            <input class="form-check-input" type="radio" name="repair-source" id="repair-source-active" checked>
                            <label class="form-check-label" for="repair-source-active">Use active DOCX</label>
                        </div>
                        <div class="form-check mb-2">
                            <input class="form-check-input" type="radio" name="repair-source" id="repair-source-upload">
                            <label class="form-check-label" for="repair-source-upload">Use a separate DOCX upload</label>
                        </div>
                        <input type="file" id="repair-file-input" accept=".docx" class="form-control form-control-sm hidden">
                        <div id="repair-source-status" class="form-text">The active DOCX will be used when available.</div>
                    </div>
                    <div class="d-grid gap-2">
                        <button class="btn btn-outline-danger repair-action-btn" data-action="soffice-roundtrip">Roundtrip with LibreOffice</button>
                        <button class="btn btn-outline-danger repair-action-btn" data-action="repair-xml">Conservative XML Repair</button>
                        <button class="btn btn-outline-danger repair-action-btn" data-action="rescue-shell">Rebuild DOCX Shell from Extracted XML</button>
                    </div>
                    <div id="repair-result" class="mt-3 small"></div>
                </div>
            </div>
        </div>

        <!-- Selection Popover (appears when text is highlighted in preview) -->
        <div id="sel-popover" class="dl-sel-popover hidden">
            <div class="dl-sel-popover-header">
                <span class="fw-semibold small">Replace with Label</span>
                <button id="sel-close" class="btn btn-sm btn-light p-0 px-1 lh-1" aria-label="Close">&times;</button>
            </div>
            <div class="dl-sel-popover-body">
                <div class="dl-sel-selected-text font-monospace mb-2" id="sel-original-text"></div>
                <label class="form-label small fw-medium mb-1" for="sel-var-input">Jinja2 replacement</label>
                <input type="text" id="sel-var-input" class="form-control form-control-sm font-monospace" placeholder="{{ users[0].name.first }}">
                <button id="sel-tree-toggle" class="dl-sel-var-tree-toggle mt-1" type="button">Browse variables…</button>
                <div id="sel-var-panel" class="dl-sel-var-panel hidden">
                    <input type="text" id="sel-var-search" class="form-control form-control-sm mb-1" placeholder="Search…">
                    <div id="sel-var-tree" class="small"></div>
                </div>
            </div>
            <div class="dl-sel-popover-footer">
                <button id="sel-cancel" class="btn btn-outline-secondary btn-sm">Cancel</button>
                <button id="sel-save" class="btn btn-primary btn-sm">Add Label</button>
            </div>
        </div>
    </div>

    <script>
    (function() {
        // ================================================================
        // AssemblyLine Variable Tree Structure
        // ================================================================
        const PERSON_ATTRIBUTES = {
            'name': {
                _description: 'Name components',
                'first': 'First name',
                'middle': 'Middle name',
                'middle_initial()': 'Middle initial',
                'last': 'Last name',
                'suffix': 'Suffix (Jr., Sr., III, etc.)',
                'full()': 'Full name'
            },
            'address': {
                _description: 'Address components',
                'block()': 'Full address (multiple lines)',
                'on_one_line()': 'Full address (single line)',
                'line_one()': 'Street + unit',
                'line_two()': 'City, state, zip',
                'address': 'Street address',
                'unit': 'Unit/Apt/Suite',
                'city': 'City',
                'state': 'State',
                'zip': 'ZIP/Postal code',
                'county': 'County',
                'country': 'Country'
            },
            'birthdate': 'Date of birth',
            'age_in_years()': 'Age (calculated)',
            'gender': 'Gender',
            'gender_female': 'Is female (checkbox)',
            'gender_male': 'Is male (checkbox)',
            'gender_other': 'Other gender (checkbox)',
            'gender_nonbinary': 'Nonbinary (checkbox)',
            'gender_undisclosed': 'Undisclosed (checkbox)',
            'phone_number': 'Phone number',
            'mobile_number': 'Mobile phone',
            'phone_numbers()': 'All phone numbers',
            'email': 'Email address',
            'signature': 'Signature'
        };

        const ATTORNEY_ATTRIBUTES = {
            ...PERSON_ATTRIBUTES,
            'bar_number': 'Bar/License number'
        };

        const AL_VARIABLE_TREE = {
            'users': { _description: 'People benefiting from the form (pro se filers)', '[0]': PERSON_ATTRIBUTES },
            'other_parties': { _description: 'Opposing/transactional parties', '[0]': PERSON_ATTRIBUTES },
            'plaintiffs': { _description: 'Plaintiffs in lawsuit', '[0]': PERSON_ATTRIBUTES },
            'defendants': { _description: 'Defendants in lawsuit', '[0]': PERSON_ATTRIBUTES },
            'petitioners': { _description: 'Petitioners', '[0]': PERSON_ATTRIBUTES },
            'respondents': { _description: 'Respondents', '[0]': PERSON_ATTRIBUTES },
            'children': { _description: 'Children involved', '[0]': PERSON_ATTRIBUTES },
            'spouses': { _description: 'Spouses', '[0]': PERSON_ATTRIBUTES },
            'parents': { _description: 'Parents', '[0]': PERSON_ATTRIBUTES },
            'caregivers': { _description: 'Caregivers', '[0]': PERSON_ATTRIBUTES },
            'guardians': { _description: 'Guardians', '[0]': PERSON_ATTRIBUTES },
            'guardians_ad_litem': { _description: 'Guardians ad litem', '[0]': PERSON_ATTRIBUTES },
            'witnesses': { _description: 'Witnesses', '[0]': PERSON_ATTRIBUTES },
            'attorneys': { _description: 'Attorneys', '[0]': ATTORNEY_ATTRIBUTES },
            'translators': { _description: 'Translators/Interpreters', '[0]': PERSON_ATTRIBUTES },
            'creditors': { _description: 'Creditors', '[0]': PERSON_ATTRIBUTES },
            'debt_collectors': { _description: 'Debt collectors', '[0]': PERSON_ATTRIBUTES },
            'decedents': { _description: 'Deceased persons', '[0]': PERSON_ATTRIBUTES },
            'interested_parties': { _description: 'Other interested parties', '[0]': PERSON_ATTRIBUTES },
            'trial_court': {
                _description: 'Court information',
                'name': 'Court name',
                'address': {
                    'county': 'County',
                    'address': 'Street address',
                    'city': 'City',
                    'state': 'State'
                },
                'division': 'Division',
                'department': 'Department'
            },
            'docket_number': 'Case/Docket number',
            'docket_numbers': 'Multiple docket numbers (comma-separated)',
            'case_name': 'Case name/caption',
            'signature_date': 'Date form is signed',
            'user_needs_interpreter': 'User needs interpreter (checkbox)',
            'user_preferred_language': 'User\'s preferred language'
        };

        // ================================================================
        // State
        // ================================================================
        let state = {
            file: null,
            fileContent: null,
            originalHtml: '',
            playgroundSource: null,
            runs: [],          // original runs from extract-runs endpoint
            existingLabels: [],
            suggestions: [],
            labelRenames: {},
            defaultModel: 'gpt-5-mini',
            recommendedModels: ['gpt-5-mini'],
            availableModels: [],
            auth: {
                isAuthenticated: false,
                email: '',
                loginUrl: '/user/sign-in',
                logoutUrl: '/user/sign-out',
                aiEnabled: false
            },
            interviewSourceMode: 'playground',
            playground: {
                projects: [],
                files: [],
                selectedProject: 'default',
                selectedFile: '',
                variables: [],
                topLevelNames: []
            },
            installed: {
                packages: [],
                files: [],
                selectedPackage: '',
                selectedFile: '',
                variables: [],
                topLevelNames: []
            },
            ui: {
                editInterviewSourceInSettings: false
            },
            settings: {
                additionalInstructions: '',
                contextText: '',
                customPeople: '',
                promptProfile: 'standard',
                model: 'gpt-5-mini',
                judgeModel: '',
                generationMethod: 'multi_run',
                generatorModels: '',
                defragmentRuns: true,
                usePlaygroundVariables: false,
                showLowConfidence: false
            },
            activeTab: 'existing',
            editingLabelId: null,
            validation: null,
            syntaxValidation: null,
            syntaxValidationPending: false,
            syntaxValidationTimer: null,
            syntaxValidationRequestToken: 0
        };

        // ================================================================
        // DOM Elements
        // ================================================================
        const fileInput = document.getElementById('file-input');
        const fileName = document.getElementById('file-name');
        const openPlaygroundBtn = document.getElementById('open-playground-btn');
        const savePlaygroundBtn = document.getElementById('save-playground-btn');
        const openPlaygroundModalEl = document.getElementById('open-playground-modal');
        const savePlaygroundModalEl = document.getElementById('save-playground-modal');
        const interviewPickerPanel = document.getElementById('interview-picker-panel');
        const interviewPickerSidebarHost = document.getElementById('interview-picker-sidebar-host');
        const settingsInterviewPickerHost = document.getElementById('settings-interview-picker-host');
        const changeInterviewSourceBtn = document.getElementById('change-interview-source-btn');
        const interviewSourceModeInputs = Array.from(document.querySelectorAll('input[name="interview-source-mode"]'));
        const playgroundFields = document.getElementById('playground-fields');
        const installedFields = document.getElementById('installed-fields');
        const playgroundProjectSelect = document.getElementById('playground-project');
        const playgroundYamlFileSelect = document.getElementById('playground-yaml-file');
        const installedPackageSelect = document.getElementById('installed-package');
        const installedYamlFileSelect = document.getElementById('installed-yaml-file');
        const interviewVariableSummary = document.getElementById('interview-variable-summary');
        const loadingState = document.getElementById('loading-state');
        const loadingMessage = document.getElementById('loading-message');
        const errorState = document.getElementById('error-state');
        const errorMessage = document.getElementById('error-message');
        const emptyState = document.getElementById('empty-state');
        const mainPanel = document.getElementById('main-panel');
        const previewEmpty = document.getElementById('preview-empty');
        const previewContent = document.getElementById('preview-content');
        const applyHighlightsBtn = document.getElementById('apply-highlights-btn');
        const downloadBtn = document.getElementById('download-btn');
        const downloadStatus = document.getElementById('download-status');
        const syntaxValidationBanner = document.getElementById('syntax-validation');
        const existingPanel = document.getElementById('existing-panel');
        const manualPanel = document.getElementById('manual-panel');
        const suggestionsPanel = document.getElementById('suggestions-panel');
        const suggestionsList = document.getElementById('suggestions-list');
        const manualLabelsList = document.getElementById('manual-labels-list');
        const existingLabelsTree = document.getElementById('existing-labels-tree');
        const existingCount = document.getElementById('existing-count');
        const manualCount = document.getElementById('manual-count');
        const suggestionsCount = document.getElementById('suggestions-count');
        const tabExisting = document.getElementById('tab-existing');
        const tabManual = document.getElementById('tab-manual');
        const tabSuggestions = document.getElementById('tab-suggestions');
        const variableTree = document.getElementById('variable-tree');
        const variableSearch = document.getElementById('variable-search');
        const settingsModal = document.getElementById('settings-modal');
        const bulkReplaceModal = document.getElementById('bulk-replace-modal');
        const editLabelModal = document.getElementById('edit-label-modal');
        const selPopover = document.getElementById('sel-popover');
        const selOriginalText = document.getElementById('sel-original-text');
        const selVarInput = document.getElementById('sel-var-input');
        const selVarPanel = document.getElementById('sel-var-panel');
        const selVarTree = document.getElementById('sel-var-tree');
        const selVarSearch = document.getElementById('sel-var-search');
        const aiModelInput = document.getElementById('ai-model');
        const aiModelSuggestions = document.getElementById('ai-model-suggestions');
        const promptProfileInput = document.getElementById('prompt-profile');
        const generationMethodInput = document.getElementById('generation-method');
        const generatorModelsGroup = document.getElementById('generator-models-group');
        const generatorModelsInput = document.getElementById('generator-models');
        const judgeModelInput = document.getElementById('judge-model');
        const usePlaygroundVariablesInput = document.getElementById('use-playground-variables');
        const usePlaygroundVarsGroup = document.getElementById('use-playground-vars-group');
        const playgroundSettingsSummary = document.getElementById('playground-settings-summary');
        const authControls = document.getElementById('auth-controls');
        const utilitiesBtn = document.getElementById('utilities-btn');
        const repairBtn = document.getElementById('repair-btn');
        const aiAuthNotice = document.getElementById('ai-auth-notice');
        const suggestionsValidation = document.getElementById('suggestions-validation');
        const toggleLowConfidence = document.getElementById('toggle-low-confidence');
        const lowConfidenceSummary = document.getElementById('low-confidence-summary');
        const utilitiesModal = document.getElementById('utilities-modal');
        const repairModal = document.getElementById('repair-modal');
        const utilityFileInput = document.getElementById('utility-file-input');
        const repairFileInput = document.getElementById('repair-file-input');
        const utilitySourceActive = document.getElementById('utility-source-active');
        const utilitySourceUpload = document.getElementById('utility-source-upload');
        const repairSourceActive = document.getElementById('repair-source-active');
        const repairSourceUpload = document.getElementById('repair-source-upload');
        const utilitiesResult = document.getElementById('utilities-result');
        const repairResult = document.getElementById('repair-result');
        const utilitySourceStatus = document.getElementById('utility-source-status');
        const repairSourceStatus = document.getElementById('repair-source-status');

        // ================================================================
        // Helpers
        // ================================================================
        function escapeHtml(text) {
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }

        function generateId() {
            return 'id-' + Math.random().toString(36).substr(2, 9);
        }

        function getConfidenceMeta(tier) {
            if (tier === 'high') {
                return { label: 'High confidence', badge: 'bg-success-subtle text-success-emphasis border border-success-subtle' };
            }
            if (tier === 'medium') {
                return { label: 'Medium confidence', badge: 'bg-warning-subtle text-warning-emphasis border border-warning-subtle' };
            }
            return { label: 'Low confidence', badge: 'bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle' };
        }

        function summarizeSuggestionSources(suggestion) {
            var sources = Array.isArray(suggestion.sources) ? suggestion.sources : [];
            var models = [];
            sources.forEach(function(source) {
                if (source && source.model && models.indexOf(source.model) === -1) {
                    models.push(source.model);
                }
            });
            return models.join(', ');
        }

        function isManualSuggestion(suggestion) {
            return !!(suggestion && suggestion._isManual);
        }

        function getManualLabelGroups() {
            return state.suggestions.filter(function(suggestion) {
                return isManualSuggestion(suggestion) && !suggestion._isCompanion;
            });
        }

        function applyManualLabelText(groupId, variableName) {
            state.suggestions.forEach(function(suggestion) {
                if (suggestion.group !== groupId) return;
                suggestion._displayLabel = variableName;
                if (suggestion._manualRole === 'single') {
                    suggestion.text = (suggestion._manualPrefix || '') + variableName + (suggestion._manualSuffix || '');
                } else if (suggestion._manualRole === 'first') {
                    suggestion.text = (suggestion._manualPrefix || '') + variableName;
                } else if (suggestion._manualRole === 'last') {
                    suggestion.text = suggestion._manualSuffix || '';
                } else if (suggestion._manualRole === 'middle') {
                    suggestion.text = '';
                }
            });
        }

        function collectRenamePayload() {
            var renames = [];
            Object.entries(state.labelRenames).forEach(function(entry) {
                var original = entry[0], newValue = entry[1];
                if (original !== newValue) {
                    renames.push({ original: original, replacement: newValue });
                }
            });
            return renames;
        }

        function collectAcceptedLabels() {
            return state.suggestions.filter(function(s) { return s.status === 'accepted'; }).map(function(s) {
                return { paragraph: s.paragraph, run: s.run, text: s.text, new_paragraph: s.new_paragraph };
            });
        }

        function renderSyntaxValidation() {
            if (!syntaxValidationBanner) return;
            var validation = state.syntaxValidation;
            var errors = validation && Array.isArray(validation.errors) ? validation.errors : [];
            var warnings = validation && Array.isArray(validation.warnings) ? validation.warnings : [];

            if (!state.syntaxValidationPending && errors.length === 0 && warnings.length === 0) {
                syntaxValidationBanner.className = 'hidden alert small mb-2';
                syntaxValidationBanner.textContent = '';
                return;
            }

            var className = 'alert small mb-2';
            var html = '';
            if (errors.length > 0) {
                className += ' alert-danger';
                html += '<div class="fw-semibold">Jinja syntax errors detected</div>';
                errors.forEach(function(issue) {
                    var label = issue.paragraph !== undefined ? 'Paragraph ' + (issue.paragraph + 1) : 'Template';
                    html += '<div>' + escapeHtml(label + ': ' + (issue.message || issue.code || 'Syntax error')) + '</div>';
                });
            } else if (warnings.length > 0) {
                className += ' alert-warning';
                html += '<div class="fw-semibold">Jinja validation warnings</div>';
                warnings.forEach(function(issue) {
                    html += '<div>' + escapeHtml(issue.message || issue.code || 'Warning') + '</div>';
                });
            } else {
                className += ' alert-info';
                html = '<div>Checking Jinja syntax...</div>';
            }

            if (state.syntaxValidationPending && (errors.length > 0 || warnings.length > 0)) {
                html += '<div class="mt-1 text-muted">Rechecking...</div>';
            }

            syntaxValidationBanner.className = className;
            syntaxValidationBanner.innerHTML = html;
        }

        async function validateCurrentSyntax(options) {
            options = options || {};
            if (!state.file) {
                state.syntaxValidation = null;
                state.syntaxValidationPending = false;
                renderSyntaxValidation();
                return null;
            }

            var renames = collectRenamePayload();
            var acceptedLabels = collectAcceptedLabels();
            if (!options.force && renames.length === 0 && acceptedLabels.length === 0) {
                state.syntaxValidation = null;
                state.syntaxValidationPending = false;
                renderSyntaxValidation();
                return null;
            }

            var requestToken = state.syntaxValidationRequestToken + 1;
            state.syntaxValidationRequestToken = requestToken;
            state.syntaxValidationPending = true;
            renderSyntaxValidation();

            try {
                var formData = new FormData();
                formData.append('file', state.file);
                formData.append('defragment_runs', state.settings.defragmentRuns ? 'true' : 'false');
                if (renames.length > 0) formData.append('renames', JSON.stringify(renames));
                if (acceptedLabels.length > 0) formData.append('labels', JSON.stringify(acceptedLabels));
                var response = await fetch('/al/docx-labeler/api/validate-syntax', { method: 'POST', body: formData });
                var data = await parseApiResponse(response);
                if (requestToken === state.syntaxValidationRequestToken) {
                    state.syntaxValidation = data.data && data.data.validation ? data.data.validation : null;
                }
            } catch (error) {
                if (requestToken === state.syntaxValidationRequestToken) {
                    state.syntaxValidation = {
                        valid: false,
                        errors: [{ code: 'validation_request_failed', message: error.message || 'Syntax validation failed.' }],
                        warnings: []
                    };
                }
            } finally {
                if (requestToken === state.syntaxValidationRequestToken) {
                    state.syntaxValidationPending = false;
                    renderSyntaxValidation();
                    updateDownloadButton();
                }
            }

            return state.syntaxValidation;
        }

        function scheduleSyntaxValidation(delay) {
            if (state.syntaxValidationTimer) {
                window.clearTimeout(state.syntaxValidationTimer);
            }
            state.syntaxValidationTimer = window.setTimeout(function() {
                validateCurrentSyntax();
            }, typeof delay === 'number' ? delay : 400);
        }

        async function ensureSyntaxValidationBeforeWrite(actionLabel) {
            var validation = await validateCurrentSyntax({ force: true });
            var errors = validation && Array.isArray(validation.errors) ? validation.errors : [];
            if (errors.length === 0) {
                return { allowed: true, validation: validation, allowInvalidSyntax: false };
            }

            var summary = errors.slice(0, 3).map(function(issue) {
                return '- ' + (issue.message || issue.code || 'Syntax error');
            }).join('\n');
            var proceed = window.confirm(
                'The Jinja syntax validator found errors before ' + actionLabel + ':\n\n'
                + summary
                + '\n\nSave anyway?'
            );
            return { allowed: proceed, validation: validation, allowInvalidSyntax: proceed };
        }

        function swapSuggestionWithAlternate(suggestion, alternateIndex) {
            if (!suggestion || !Array.isArray(suggestion.alternates)) return;
            var alternate = suggestion.alternates[alternateIndex];
            if (!alternate) return;
            var currentPrimary = {
                text: suggestion.text,
                paragraph: suggestion.paragraph,
                run: suggestion.run,
                new_paragraph: suggestion.new_paragraph,
                validation_flags: suggestion.validation_flags || [],
                judge_review: suggestion.judge_review || null,
                confidence: suggestion.confidence || 'low',
                vote_count: suggestion.vote_count || 0,
                clean_vote_count: suggestion.clean_vote_count || 0,
                vote_total: suggestion.vote_total || 0,
                sources: suggestion.sources || []
            };
            suggestion.text = alternate.text;
            suggestion.validation_flags = alternate.validation_flags || [];
            suggestion.judge_review = alternate.judge_review || null;
            suggestion.confidence = alternate.confidence || 'low';
            suggestion.vote_count = alternate.vote_count || 0;
            suggestion.clean_vote_count = alternate.clean_vote_count || 0;
            suggestion.vote_total = alternate.vote_total || suggestion.vote_total || 0;
            suggestion.sources = alternate.sources || [];
            suggestion.alternates.splice(alternateIndex, 1, currentPrimary);
        }

        async function fetchModelCatalog() {
            try {
                var response = await fetch('/al/labeler/api/models', { method: 'GET' });
                var data = await response.json();
                if (!data.success || !data.data) return;
                state.defaultModel = data.data.default_model || state.defaultModel;
                state.recommendedModels = Array.isArray(data.data.recommended_models) && data.data.recommended_models.length
                    ? data.data.recommended_models
                    : [state.defaultModel];
                state.availableModels = Array.isArray(data.data.available_models) ? data.data.available_models : [];
                state.settings.model = state.defaultModel;
                renderModelSuggestions('');
            } catch (_error) {
                renderModelSuggestions('');
            }
        }

        function renderAuthControls() {
            if (!authControls) return;
            if (state.auth.isAuthenticated) {
                var emailText = state.auth.email || 'Account';
                authControls.innerHTML =
                    '<button id="auth-menu-btn" class="btn btn-outline-light btn-sm dropdown-toggle" type="button" aria-expanded="false">' + escapeHtml(emailText) + '</button>' +
                    '<div id="auth-menu" class="dropdown-menu dropdown-menu-end">' +
                        '<a class="dropdown-item" href="' + escapeHtml(state.auth.logoutUrl || '/user/sign-out') + '">Log out</a>' +
                    '</div>';
                var menuBtn = document.getElementById('auth-menu-btn');
                var menu = document.getElementById('auth-menu');
                menuBtn.addEventListener('click', function(e) {
                    e.stopPropagation();
                    menu.classList.toggle('show');
                });
            } else {
                authControls.innerHTML =
                    '<a class="btn btn-outline-light btn-sm" href="' + escapeHtml(state.auth.loginUrl || '/user/sign-in') + '">Log in</a>';
            }
        }

        function updateAiUiState() {
            var aiEnabled = !!state.auth.aiEnabled;
            var regenerateBtn = document.getElementById('regenerate-btn');
            if (regenerateBtn) {
                regenerateBtn.disabled = !aiEnabled;
            }
            if (!aiEnabled) {
                aiAuthNotice.classList.remove('hidden');
                aiAuthNotice.innerHTML = 'AI labeling requires login. <a href="' + escapeHtml(state.auth.loginUrl || '/user/sign-in') + '">Log in</a> to enable suggestions.';
            } else {
                aiAuthNotice.classList.add('hidden');
            }
            // Show playground buttons only when authenticated
            if (state.auth.isAuthenticated) {
                openPlaygroundBtn.classList.remove('hidden');
            } else {
                openPlaygroundBtn.classList.add('hidden');
                savePlaygroundBtn.classList.add('hidden');
            }
        }

        function getActiveInterviewSourceState() {
            return state.interviewSourceMode === 'installed' ? state.installed : state.playground;
        }

        function moveInterviewPickerTo(hostElement) {
            if (!hostElement || !interviewPickerPanel) return;
            if (interviewPickerPanel.parentElement !== hostElement) {
                hostElement.appendChild(interviewPickerPanel);
            }
        }

        function getActiveInterviewSourceLabel() {
            var sourceState = getActiveInterviewSourceState();
            if (state.interviewSourceMode === 'installed') {
                if (!sourceState.selectedPackage || !sourceState.selectedFile) return '';
                return sourceState.selectedPackage + '/' + sourceState.selectedFile;
            }
            if (!sourceState.selectedProject || !sourceState.selectedFile) return '';
            return sourceState.selectedProject + '/' + sourceState.selectedFile;
        }

        function renderInterviewSourceSummary() {
            if (!interviewVariableSummary || !playgroundSettingsSummary) return;
            var sourceLabel = getActiveInterviewSourceLabel();
            var sourceState = getActiveInterviewSourceState();
            var sourceText = state.interviewSourceMode === 'installed' ? 'installed interview' : 'Playground interview';
            var count = sourceState.variables.length;
            if (sourceLabel) {
                interviewVariableSummary.textContent =
                    sourceLabel
                    + (count ? ' (' + count + ' vars, ' + sourceState.topLevelNames.length + ' top-level)' : ' (no variables detected yet)');
                interviewVariableSummary.classList.remove('hidden');
                playgroundSettingsSummary.textContent =
                    'Selected ' + sourceText + ': ' + sourceLabel
                    + (count ? '. The prompt will prefer its existing variable names.' : '.');
            } else {
                interviewVariableSummary.classList.add('hidden');
                playgroundSettingsSummary.textContent =
                    'Select a Playground or installed interview from the sidebar to use its variable list in the prompt.';
            }
        }

        function renderInterviewPicker() {
            if (!interviewPickerPanel || !interviewPickerSidebarHost || !settingsInterviewPickerHost || !usePlaygroundVarsGroup) return;
            var showPicker = !!state.auth.isAuthenticated;
            usePlaygroundVarsGroup.classList.toggle('hidden', !showPicker);
            if (changeInterviewSourceBtn) {
                changeInterviewSourceBtn.classList.toggle('hidden', !showPicker);
                changeInterviewSourceBtn.textContent = state.ui.editInterviewSourceInSettings ? 'Done' : 'Change';
            }
            if (showPicker && state.ui.editInterviewSourceInSettings) {
                moveInterviewPickerTo(settingsInterviewPickerHost);
            } else {
                moveInterviewPickerTo(interviewPickerSidebarHost);
            }
            interviewPickerPanel.classList.toggle('hidden', !showPicker);
            interviewPickerSidebarHost.classList.toggle('hidden', !showPicker || state.ui.editInterviewSourceInSettings);
            settingsInterviewPickerHost.classList.toggle('hidden', !showPicker || !state.ui.editInterviewSourceInSettings);
            playgroundFields.classList.toggle('hidden', state.interviewSourceMode !== 'playground');
            installedFields.classList.toggle('hidden', state.interviewSourceMode !== 'installed');
            interviewSourceModeInputs.forEach(function(input) {
                input.checked = input.value === state.interviewSourceMode;
            });

            playgroundProjectSelect.innerHTML = '';
            state.playground.projects.forEach(function(projectName) {
                var option = document.createElement('option');
                option.value = projectName;
                option.textContent = projectName;
                if (projectName === state.playground.selectedProject) option.selected = true;
                playgroundProjectSelect.appendChild(option);
            });

            playgroundYamlFileSelect.innerHTML = '';
            if (!state.playground.files.length) {
                var emptyPlaygroundOption = document.createElement('option');
                emptyPlaygroundOption.value = '';
                emptyPlaygroundOption.textContent = 'No YAML files found';
                playgroundYamlFileSelect.appendChild(emptyPlaygroundOption);
            } else {
                state.playground.files.forEach(function(fileInfo) {
                    var option = document.createElement('option');
                    option.value = fileInfo.filename;
                    option.textContent = fileInfo.label || fileInfo.filename;
                    if (fileInfo.filename === state.playground.selectedFile) option.selected = true;
                    playgroundYamlFileSelect.appendChild(option);
                });
            }

            installedPackageSelect.innerHTML = '';
            state.installed.packages.forEach(function(packageName) {
                var option = document.createElement('option');
                option.value = packageName;
                option.textContent = packageName;
                if (packageName === state.installed.selectedPackage) option.selected = true;
                installedPackageSelect.appendChild(option);
            });

            installedYamlFileSelect.innerHTML = '';
            if (!state.installed.files.length) {
                var emptyInstalledOption = document.createElement('option');
                emptyInstalledOption.value = '';
                emptyInstalledOption.textContent = 'No YAML files found';
                installedYamlFileSelect.appendChild(emptyInstalledOption);
            } else {
                state.installed.files.forEach(function(fileInfo) {
                    var option = document.createElement('option');
                    option.value = fileInfo.filename;
                    option.textContent = fileInfo.label || fileInfo.filename;
                    if (fileInfo.filename === state.installed.selectedFile) option.selected = true;
                    installedYamlFileSelect.appendChild(option);
                });
            }

            var activeSourceState = getActiveInterviewSourceState();
            usePlaygroundVariablesInput.disabled = !showPicker || !activeSourceState.selectedFile;
            if (usePlaygroundVariablesInput.disabled) {
                state.settings.usePlaygroundVariables = false;
                usePlaygroundVariablesInput.checked = false;
            }
            renderInterviewSourceSummary();
        }

        function deriveTopLevelNames(variableNames) {
            return Array.from(new Set(
                (Array.isArray(variableNames) ? variableNames : [])
                    .map(function(name) { return String(name || '').trim(); })
                    .filter(function(name) { return !!name; })
                    .map(function(name) { return name.split('.', 1)[0].split('[', 1)[0]; })
                    .filter(function(name) { return !!name; })
            )).sort(function(a, b) { return a.localeCompare(b); });
        }

        function splitVariablePathSegments(variableName) {
            var raw = String(variableName || '').trim();
            if (!raw) return [];
            var matches = raw.match(/([^[.\]]+|\[[^\]]*\])/g);
            return Array.isArray(matches) ? matches.filter(function(part) { return !!part; }) : [raw];
        }

        function buildInterviewVariableTree(variableNames) {
            var root = {
                _description: 'Variables detected from the selected interview'
            };
            (Array.isArray(variableNames) ? variableNames : []).forEach(function(variableName) {
                var segments = splitVariablePathSegments(variableName);
                if (!segments.length) return;
                var node = root;
                segments.forEach(function(segment, index) {
                    var isLeaf = index === segments.length - 1;
                    if (!node[segment] || typeof node[segment] !== 'object') {
                        node[segment] = {};
                    }
                    if (isLeaf) {
                        node[segment]._variable = true;
                        node[segment]._description = 'Selected interview variable';
                    }
                    node = node[segment];
                });
            });
            return root;
        }

        function getEffectiveVariableTree() {
            var sourceState = getActiveInterviewSourceState();
            if (!sourceState.variables.length) {
                return AL_VARIABLE_TREE;
            }
            return Object.assign(
                {
                    'Selected interview variables': buildInterviewVariableTree(sourceState.variables)
                },
                AL_VARIABLE_TREE
            );
        }

        async function fetchJsonOrThrow(url) {
            var response = await fetch(url, {
                method: 'GET',
                credentials: 'same-origin'
            });
            if (!response.ok) {
                throw new Error('Request failed with status ' + response.status);
            }
            return response.json();
        }

        async function parseApiResponse(response) {
            var contentType = response.headers.get('content-type') || '';
            var data;
            if (contentType.indexOf('application/json') !== -1) {
                data = await response.json();
            } else {
                var text = await response.text();
                if (!response.ok) {
                    throw new Error(text || ('Request failed (' + response.status + ').'));
                }
                throw new Error('The server returned a non-JSON response.');
            }
            if (!response.ok) {
                throw new Error((data && data.error && data.error.message) ? data.error.message : ('Request failed (' + response.status + ').'));
            }
            return data;
        }

        function updateModalSourceStatus() {
            var hasActiveFile = !!state.file;
            utilityFileInput.classList.toggle('hidden', utilitySourceActive.checked);
            repairFileInput.classList.toggle('hidden', repairSourceActive.checked);
            utilitySourceStatus.textContent = utilitySourceActive.checked
                ? (hasActiveFile ? 'The active DOCX will be used.' : 'No active DOCX is loaded. Choose a separate DOCX upload instead.')
                : (utilityFileInput.files[0] ? utilityFileInput.files[0].name : 'Choose a DOCX file for utilities.');
            repairSourceStatus.textContent = repairSourceActive.checked
                ? (hasActiveFile ? 'The active DOCX will be used.' : 'No active DOCX is loaded. Choose a separate DOCX upload instead.')
                : (repairFileInput.files[0] ? repairFileInput.files[0].name : 'Choose a DOCX file for repair.');
        }

        function getModalSourceFile(kind) {
            if (kind === 'utility') {
                return utilitySourceActive.checked ? state.file : utilityFileInput.files[0];
            }
            return repairSourceActive.checked ? state.file : repairFileInput.files[0];
        }

        function downloadBase64Docx(docxBase64, filename) {
            var binaryString = atob(docxBase64);
            var bytes = new Uint8Array(binaryString.length);
            for (var i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
            var blob = new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
            var url = URL.createObjectURL(blob);
            var link = document.createElement('a');
            link.href = url;
            link.download = filename || 'output.docx';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
        }

        function formatReportAsHtml(report) {
            if (!report) return '<div class="text-muted">No report returned.</div>';
            if (typeof report === 'string') return '<div>' + escapeHtml(report) + '</div>';
            if (Array.isArray(report)) return '<pre class="bg-light rounded p-2 small mb-0">' + escapeHtml(JSON.stringify(report, null, 2)) + '</pre>';

            var html = '';
            if (Array.isArray(report.errors) && report.errors.length) {
                html += '<div class="alert alert-danger small"><div class="fw-semibold mb-1">Errors</div>';
                report.errors.forEach(function(issue) {
                    html += '<div>' + escapeHtml(issue.message || issue.error || JSON.stringify(issue)) + '</div>';
                });
                html += '</div>';
            }
            if (Array.isArray(report.warning_details) && report.warning_details.length) {
                html += '<div class="alert alert-warning small"><div class="fw-semibold mb-1">Warnings</div>';
                report.warning_details.forEach(function(issue) {
                    html += '<div>' + escapeHtml(issue.message || issue.code || JSON.stringify(issue)) + '</div>';
                });
                html += '</div>';
            } else if (Array.isArray(report.warnings) && report.warnings.length) {
                html += '<div class="alert alert-warning small"><div class="fw-semibold mb-1">Warnings</div>';
                report.warnings.forEach(function(issue) {
                    html += '<div>' + escapeHtml(typeof issue === 'string' ? issue : (issue.message || issue.code || JSON.stringify(issue))) + '</div>';
                });
                html += '</div>';
            }
            if (Array.isArray(report.xml_parse_errors) && report.xml_parse_errors.length) {
                html += '<div class="alert alert-danger small"><div class="fw-semibold mb-1">XML Parse Errors</div>';
                report.xml_parse_errors.forEach(function(issue) {
                    html += '<div>' + escapeHtml((issue.part || 'part') + ': ' + (issue.error || 'Parse error')) + '</div>';
                });
                html += '</div>';
            }
            if (Array.isArray(report.schema_errors) && report.schema_errors.length) {
                html += '<div class="alert alert-danger small"><div class="fw-semibold mb-1">Schema Errors</div>';
                report.schema_errors.forEach(function(issue) {
                    html += '<div>' + escapeHtml((issue.part || 'part') + ': ' + (issue.error || 'Schema error')) + '</div>';
                });
                html += '</div>';
            }
            if (report.message) {
                html += '<div class="alert alert-info small">' + escapeHtml(report.message) + '</div>';
            }
            html += '<pre class="bg-light rounded p-2 small mb-0">' + escapeHtml(JSON.stringify(report, null, 2)) + '</pre>';
            return html;
        }

        async function runDocxOperation(kind, action, progressMessage) {
            var sourceFile = getModalSourceFile(kind);
            if (!sourceFile) {
                showError('Choose a DOCX file or load one into the editor first.');
                return;
            }

            var resultContainer = kind === 'utility' ? utilitiesResult : repairResult;
            var endpoint = kind === 'utility' ? '/al/docx-labeler/api/utilities' : '/al/docx-labeler/api/repair';
            resultContainer.innerHTML = '<div class="text-muted">Running...</div>';
            showLoading(progressMessage);
            try {
                var formData = new FormData();
                formData.append('file', sourceFile);
                formData.append('action', action);
                var response = await fetch(endpoint, { method: 'POST', body: formData });
                var payload = await parseApiResponse(response);
                var data = payload.data || {};
                var html = formatReportAsHtml(data.report);
                if (data.docx_base64) {
                    html += '<div class="mt-3"><button class="btn btn-primary btn-sm" id="' + kind + '-download-result">Download Result</button></div>';
                }
                resultContainer.innerHTML = html;
                if (data.docx_base64) {
                    document.getElementById(kind + '-download-result').addEventListener('click', function() {
                        downloadBase64Docx(data.docx_base64, data.filename);
                    });
                }
            } catch (error) {
                resultContainer.innerHTML = '<div class="alert alert-danger small mb-0">' + escapeHtml(error.message || 'Operation failed.') + '</div>';
            } finally {
                hideLoading();
            }
        }

        function sleep(milliseconds) {
            return new Promise(function(resolve) {
                window.setTimeout(resolve, milliseconds);
            });
        }

        async function pollLabelerJob(jobUrl, progressMessage) {
            var startedAt = Date.now();
            var timeoutMs = 15 * 60 * 1000;
            while ((Date.now() - startedAt) < timeoutMs) {
                await sleep(1500);
                var response = await fetch(jobUrl, {
                    method: 'GET',
                    headers: { 'Accept': 'application/json' }
                });
                var payload = await parseApiResponse(response);
                if (payload.status === 'queued' || payload.status === 'running') {
                    showLoading(progressMessage);
                    continue;
                }
                if (payload.status === 'failed') {
                    throw new Error((payload.error && payload.error.message) || 'Background job failed.');
                }
                if (payload.status === 'succeeded') {
                    return payload.data || {};
                }
                throw new Error('Unexpected background job status.');
            }
            throw new Error('The background job timed out before it completed.');
        }

        async function requestAsyncLabelerAction(endpointPath, formData, progressMessage) {
            formData.append('mode', 'async');
            var response = await fetch(endpointPath, {
                method: 'POST',
                headers: { 'Accept': 'application/json' },
                body: formData
            });
            var payload = await parseApiResponse(response);
            if (payload.status === 'queued' && payload.job_url) {
                showLoading(progressMessage);
                return pollLabelerJob(payload.job_url, progressMessage);
            }
            return (payload && payload.data) ? payload.data : {};
        }

        async function fetchPlaygroundProjects() {
            if (!state.auth.isAuthenticated) {
                state.playground.projects = [];
                state.playground.files = [];
                state.playground.selectedFile = '';
                state.playground.variables = [];
                state.playground.topLevelNames = [];
                renderInterviewPicker();
                return;
            }
            try {
                var data = await fetchJsonOrThrow('/al/labeler/api/playground-projects');
                state.playground.projects =
                    data && data.success && data.data && Array.isArray(data.data.projects)
                        ? data.data.projects
                        : [];
                if (state.playground.projects.indexOf(state.playground.selectedProject) === -1) {
                    state.playground.selectedProject = state.playground.projects[0] || 'default';
                }
                await fetchPlaygroundFiles();
                return;
            } catch (_error) {}
            state.playground.projects = [];
            state.playground.files = [];
            state.playground.selectedFile = '';
            state.playground.variables = [];
            state.playground.topLevelNames = [];
            renderInterviewPicker();
        }

        async function fetchPlaygroundFiles() {
            if (!state.auth.isAuthenticated || !state.playground.selectedProject) {
                state.playground.files = [];
                state.playground.selectedFile = '';
                state.playground.variables = [];
                state.playground.topLevelNames = [];
                renderInterviewPicker();
                return;
            }
            try {
                var data = await fetchJsonOrThrow(
                    '/al/labeler/api/playground-files?project='
                    + encodeURIComponent(state.playground.selectedProject)
                );
                var files =
                    data && data.success && data.data && Array.isArray(data.data.files)
                        ? data.data.files
                        : [];
                state.playground.files = files;
                var currentStillExists = files.some(function(item) { return item.filename === state.playground.selectedFile; });
                state.playground.selectedFile = currentStillExists ? state.playground.selectedFile : (files[0] ? files[0].filename : '');
                renderInterviewPicker();
                await fetchPlaygroundVariables();
                return;
            } catch (_error) {}
            state.playground.files = [];
            state.playground.selectedFile = '';
            state.playground.variables = [];
            state.playground.topLevelNames = [];
            renderInterviewPicker();
        }

        async function fetchPlaygroundVariables() {
            if (!state.auth.isAuthenticated || !state.playground.selectedProject || !state.playground.selectedFile) {
                state.playground.variables = [];
                state.playground.topLevelNames = [];
                renderInterviewPicker();
                return;
            }
            try {
                var data = await fetchJsonOrThrow(
                    '/al/labeler/api/playground-variables?project='
                    + encodeURIComponent(state.playground.selectedProject)
                    + '&filename=' + encodeURIComponent(state.playground.selectedFile)
                );
                state.playground.variables =
                    data && data.success && data.data && Array.isArray(data.data.all_names)
                        ? data.data.all_names
                        : [];
                state.playground.topLevelNames =
                    data && data.success && data.data && Array.isArray(data.data.top_level_names)
                        ? data.data.top_level_names
                        : deriveTopLevelNames(state.playground.variables);
            } catch (_error) {
                state.playground.variables = [];
                state.playground.topLevelNames = [];
            }
            renderInterviewPicker();
        }

        async function fetchInstalledPackages() {
            if (!state.auth.isAuthenticated) {
                state.installed.packages = [];
                state.installed.files = [];
                state.installed.selectedPackage = '';
                state.installed.selectedFile = '';
                state.installed.variables = [];
                state.installed.topLevelNames = [];
                renderInterviewPicker();
                return;
            }
            try {
                var data = await fetchJsonOrThrow('/al/labeler/api/installed-packages');
                state.installed.packages =
                    data && data.success && data.data && Array.isArray(data.data.packages)
                        ? data.data.packages
                        : [];
                if (state.installed.packages.indexOf(state.installed.selectedPackage) === -1) {
                    state.installed.selectedPackage = state.installed.packages[0] || '';
                }
                await fetchInstalledFiles();
                return;
            } catch (_error) {}
            state.installed.packages = [];
            state.installed.files = [];
            state.installed.selectedPackage = '';
            state.installed.selectedFile = '';
            state.installed.variables = [];
            state.installed.topLevelNames = [];
            renderInterviewPicker();
        }

        async function fetchInstalledFiles() {
            if (!state.auth.isAuthenticated || !state.installed.selectedPackage) {
                state.installed.files = [];
                state.installed.selectedFile = '';
                state.installed.variables = [];
                state.installed.topLevelNames = [];
                renderInterviewPicker();
                return;
            }
            try {
                var data = await fetchJsonOrThrow(
                    '/al/labeler/api/installed-files?package='
                    + encodeURIComponent(state.installed.selectedPackage)
                );
                var files =
                    data && data.success && data.data && Array.isArray(data.data.files)
                        ? data.data.files
                        : [];
                state.installed.files = files;
                var currentStillExists = files.some(function(item) { return item.filename === state.installed.selectedFile; });
                state.installed.selectedFile = currentStillExists ? state.installed.selectedFile : (files[0] ? files[0].filename : '');
                renderInterviewPicker();
                await fetchInstalledVariables();
                return;
            } catch (_error) {}
            state.installed.files = [];
            state.installed.selectedFile = '';
            state.installed.variables = [];
            state.installed.topLevelNames = [];
            renderInterviewPicker();
        }

        async function fetchInstalledVariables() {
            if (!state.auth.isAuthenticated || !state.installed.selectedPackage || !state.installed.selectedFile) {
                state.installed.variables = [];
                state.installed.topLevelNames = [];
                renderInterviewPicker();
                return;
            }
            try {
                var interviewPath = state.installed.selectedPackage + ':' + state.installed.selectedFile;
                var data = await fetchJsonOrThrow(
                    '/al/labeler/api/installed-variables?interview_path='
                    + encodeURIComponent(interviewPath)
                );
                state.installed.variables =
                    data && data.success && data.data && Array.isArray(data.data.all_names)
                        ? data.data.all_names
                        : [];
                state.installed.topLevelNames =
                    data && data.success && data.data && Array.isArray(data.data.top_level_names)
                        ? data.data.top_level_names
                        : deriveTopLevelNames(state.installed.variables);
            } catch (_error) {
                state.installed.variables = [];
                state.installed.topLevelNames = [];
            }
            renderInterviewPicker();
        }

        async function fetchAuthStatus() {
            try {
                var nextTarget = window.location.pathname + window.location.search;
                var response = await fetch('/al/labeler/api/auth-status?next=' + encodeURIComponent(nextTarget), { method: 'GET' });
                var data = await response.json();
                if (data.success && data.data) {
                    state.auth.isAuthenticated = !!data.data.is_authenticated;
                    state.auth.email = data.data.email || '';
                    state.auth.loginUrl = data.data.login_url || '/user/sign-in';
                    state.auth.logoutUrl = data.data.logout_url || '/user/sign-out';
                    state.auth.aiEnabled = !!data.data.ai_enabled;
                }
            } catch (_error) {
                state.auth = {
                    isAuthenticated: false,
                    email: '',
                    loginUrl: '/user/sign-in',
                    logoutUrl: '/user/sign-out',
                    aiEnabled: false
                };
            }
            renderAuthControls();
            updateAiUiState();
            await fetchPlaygroundProjects();
            await fetchInstalledPackages();
        }

        function renderModelSuggestions(filterText) {
            var filter = (filterText || '').trim().toLowerCase();
            var modelChoices = [];
            if (filter) {
                var source = state.availableModels.length ? state.availableModels : state.recommendedModels;
                modelChoices = source.filter(function(model) {
                    return model.toLowerCase().includes(filter);
                }).slice(0, 20);
            } else {
                modelChoices = state.recommendedModels.slice(0, 8);
            }
            aiModelSuggestions.innerHTML = '';
            modelChoices.forEach(function(modelName) {
                var button = document.createElement('button');
                button.type = 'button';
                button.className = 'btn btn-sm btn-outline-secondary font-monospace';
                button.textContent = modelName;
                button.addEventListener('click', function() {
                    aiModelInput.value = modelName;
                    state.settings.model = modelName;
                    renderModelSuggestions(modelName);
                });
                aiModelSuggestions.appendChild(button);
            });
        }

        function renderGenerationMethodFields() {
            var method = generationMethodInput.value || state.settings.generationMethod || 'multi_run';
            generatorModelsGroup.classList.toggle('hidden', method !== 'multi_model');
        }

        function parseGeneratorModelsInput(rawValue) {
            return (rawValue || '')
                .split(/[\n,]/)
                .map(function(item) { return item.trim(); })
                .filter(function(item) { return item.length > 0; });
        }

        // ================================================================
        // UI state transitions
        // ================================================================
        function showLoading(message) {
            loadingMessage.textContent = message;
            loadingState.classList.remove('hidden');
            emptyState.classList.add('hidden');
            mainPanel.classList.add('hidden');
            errorState.classList.add('hidden');
        }

        function hideLoading() {
            loadingState.classList.add('hidden');
        }

        function showError(message) {
            errorMessage.textContent = message;
            errorState.classList.remove('hidden');
            emptyState.classList.add('hidden');
            mainPanel.classList.add('hidden');
        }

        function showMainPanel() {
            mainPanel.classList.remove('hidden');
            emptyState.classList.add('hidden');
            errorState.classList.add('hidden');
        }

        // ================================================================
        // Label extraction from mammoth HTML
        // ================================================================
        function extractExistingLabels(html) {
            const labels = [];
            const seen = new Map();
            const varPattern = /\{\{[\s\S]*?\}\}/g;
            let match;
            while ((match = varPattern.exec(html)) !== null) {
                const label = match[0];
                if (seen.has(label)) {
                    seen.get(label).occurrences++;
                } else {
                    const entry = { id: generateId(), original: label, current: label, occurrences: 1 };
                    seen.set(label, entry);
                    labels.push(entry);
                }
            }
            const ctrlPattern = /\{%[\s\S]*?%\}/g;
            while ((match = ctrlPattern.exec(html)) !== null) {
                const label = match[0];
                if (seen.has(label)) {
                    seen.get(label).occurrences++;
                } else {
                    const entry = { id: generateId(), original: label, current: label, occurrences: 1, isControl: true };
                    seen.set(label, entry);
                    labels.push(entry);
                }
            }
            return labels;
        }

        // ================================================================
        // Paragraph / run index for mapping selections to runs
        // ================================================================
        function buildParagraphIndex() {
            var index = {};
            state.runs.forEach(function(r) {
                var para = r[0], run = r[1], text = r[2];
                if (!index[para]) index[para] = { runs: [], fullText: '' };
                index[para].runs.push({ runIdx: run, text: text, start: index[para].fullText.length, end: 0 });
                index[para].fullText += text;
            });
            Object.keys(index).forEach(function(p) {
                index[p].runs.forEach(function(r) { r.end = r.start + r.text.length; });
            });
            return index;
        }

        /**
         * Given a user-selected text string, find the paragraph and set of
         * consecutive runs whose concatenated text contains the selection.
         * Returns null when no match is found.
         */
        function findRunsForSelection(selectedText) {
            if (!selectedText || !selectedText.trim()) return null;
            var paraIndex = buildParagraphIndex();
            var trimmed = selectedText.trim();

            for (var paraNum in paraIndex) {
                var para = paraIndex[paraNum];
                // Direct substring lookup in this paragraph’s concatenated text
                var pos = para.fullText.indexOf(trimmed);
                if (pos === -1) {
                    // Normalised whitespace fallback
                    var normFull = para.fullText.replace(/\s+/g, ' ');
                    var normSel  = trimmed.replace(/\s+/g, ' ');
                    pos = normFull.indexOf(normSel);
                    if (pos === -1) continue;
                }
                var end = pos + trimmed.length;

                var matchedRuns = [];
                para.runs.forEach(function(r) {
                    if (r.end > pos && r.start < end) {
                        matchedRuns.push({
                            paragraph: parseInt(paraNum),
                            run: r.runIdx,
                            originalText: r.text,
                            selStart: Math.max(0, pos - r.start),
                            selEnd:   Math.min(r.text.length, end - r.start),
                            isFullySelected: pos <= r.start && end >= r.end
                        });
                    }
                });
                if (matchedRuns.length > 0) {
                    return { paraNum: parseInt(paraNum), runs: matchedRuns, selectedText: trimmed };
                }
            }
            return null;
        }

        /**
         * Build suggestion entries that replace the matched runs with a Jinja2
         * variable.  The first run keeps any unselected prefix and gets the
         * label; the last run keeps any unselected suffix; middle runs are
         * blanked.  All entries share a `group` id so the UI and preview can
         * treat them as one logical operation.
         */
        function createLabelFromSelection(variableName, match) {
            var groupId = generateId();
            var labels = [];
            match.runs.forEach(function(r, i) {
                var newText;
                var manualRole;
                var manualPrefix = '';
                var manualSuffix = '';
                if (match.runs.length === 1) {
                    // Only run: prefix + label + suffix
                    manualPrefix = r.originalText.substring(0, r.selStart);
                    manualSuffix = r.originalText.substring(r.selEnd);
                    newText = manualPrefix + variableName + manualSuffix;
                    manualRole = 'single';
                } else if (i === 0) {
                    manualPrefix = r.originalText.substring(0, r.selStart);
                    newText = manualPrefix + variableName;
                    manualRole = 'first';
                } else if (i === match.runs.length - 1) {
                    manualSuffix = r.originalText.substring(r.selEnd);
                    newText = manualSuffix;
                    manualRole = 'last';
                } else {
                    newText = '';
                    manualRole = 'middle';
                }
                labels.push({
                    paragraph: r.paragraph,
                    run: r.run,
                    text: newText,
                    new_paragraph: 0,
                    id: generateId(),
                    status: 'accepted',
                    group: groupId,
                    _isCompanion: i > 0,
                    _isManual: true,
                    _selectedText: match.selectedText,
                    _displayLabel: variableName,
                    _manualRole: manualRole,
                    _manualPrefix: manualPrefix,
                    _manualSuffix: manualSuffix
                });
            });
            return labels;
        }

        // ================================================================
        // Selection popover logic
        // ================================================================
        var _selMatch = null;   // stashed findRunsForSelection() result

        function showSelectionPopover(match, mouseX, mouseY) {
            _selMatch = match;
            selOriginalText.textContent = match.selectedText;
            selVarInput.value = '';
            selVarPanel.classList.add('hidden');
            selVarSearch.value = '';

            // Position near the cursor, clamped to viewport
            var pw = 380, ph = 260;
            var vw = window.innerWidth, vh = window.innerHeight;
            var left = Math.min(mouseX + 4, vw - pw - 12);
            var top  = mouseY + 12;
            if (top + ph > vh) top = Math.max(8, mouseY - ph - 8);
            selPopover.style.left = Math.max(4, left) + 'px';
            selPopover.style.top  = top + 'px';
            selPopover.classList.remove('hidden');
            selVarInput.focus();
        }

        function hideSelectionPopover() {
            selPopover.classList.add('hidden');
            _selMatch = null;
        }

        function saveSelectionLabel() {
            var raw = selVarInput.value.trim();
            if (!raw || !_selMatch) return;
            // Wrap in {{ }} if the user didn’t
            var varName = raw;
            if (!/^\{[{%]/.test(varName)) {
                varName = '{{ ' + varName + ' }}';
            }
            var newLabels = createLabelFromSelection(varName, _selMatch);
            state.suggestions = state.suggestions.concat(newLabels);
            hideSelectionPopover();
            renderSuggestions();
            updatePreview();
            updateDownloadButton();
            switchTab('manual');
        }

        function onPreviewMouseUp(e) {
            // Ignore if popover is already open or no document loaded
            if (!selPopover.classList.contains('hidden')) return;
            if (!state.file || state.runs.length === 0) return;

            var sel = window.getSelection();
            if (!sel || sel.isCollapsed) return;
            var text = sel.toString();
            if (!text || !text.trim()) return;

            // Make sure the selection is inside the preview area
            var anchor = sel.anchorNode, focus = sel.focusNode;
            if (!previewContent.contains(anchor) || !previewContent.contains(focus)) return;

            var match = findRunsForSelection(text);
            if (!match) return;

            showSelectionPopover(match, e.clientX, e.clientY);
        }

        // Render the variable tree inside the popover (reuses the same builder)
        function renderSelPopoverTree(filter) {
            selVarTree.innerHTML = '';
            renderVariableTree(getEffectiveVariableTree(), '', selVarTree, filter || '', function(varPath) {
                selVarInput.value = '{{ ' + varPath + ' }}';
                selVarInput.focus();
            });
        }

        // ================================================================
        // Preview rendering with highlights
        // ================================================================
        function updatePreview() {
            let html = state.originalHtml;

            // --- Grouped selection labels (multi-run) ---
            // Replace the full selected text span with the display label for
            // each unique group, before handling single-run suggestions.
            var processedGroups = {};
            state.suggestions.forEach(function(s) {
                if (s.status === 'accepted' && s.group && !processedGroups[s.group]) {
                    processedGroups[s.group] = true;
                    var displayLabel = s._displayLabel || s.text;
                    var selectedText = s._selectedText;
                    if (selectedText) {
                        var encoded = escapeHtml(selectedText);
                        var pos = html.indexOf(encoded);
                        if (pos !== -1) {
                            html = html.substring(0, pos)
                                + '<span class="highlight-accepted">' + escapeHtml(displayLabel) + '</span>'
                                + html.substring(pos + encoded.length);
                        }
                    }
                }
            });

            // --- Single-run AI suggestions (no group) ---
            const acceptedByKey = {};
            state.suggestions.forEach(function(s) {
                if (s.status === 'accepted' && s.new_paragraph === 0 && !s.group) {
                    acceptedByKey[s.paragraph + ',' + s.run] = s;
                }
            });

            const replacements = [];
            for (const key of Object.keys(acceptedByKey)) {
                const s = acceptedByKey[key];
                const run = state.runs.find(function(r) {
                    return r[0] === s.paragraph && r[1] === s.run;
                });
                if (run && run[2] && run[2].trim()) {
                    replacements.push({ original: run[2], replacement: s.text });
                }
            }
            replacements.sort(function(a, b) { return b.original.length - a.original.length; });

            for (const r of replacements) {
                const encoded = escapeHtml(r.original);
                const pos = html.indexOf(encoded);
                if (pos !== -1) {
                    html = html.substring(0, pos)
                        + '<span class="highlight-accepted">' + escapeHtml(r.replacement) + '</span>'
                        + html.substring(pos + encoded.length);
                }
            }

            // Highlight existing labels (with any renames applied)
            state.existingLabels.forEach(function(label) {
                const cls = label.original !== label.current ? 'highlight-accepted' : 'highlight-existing';
                html = html.split(escapeHtml(label.original)).join(
                    '<span class="' + cls + '">' + escapeHtml(label.current) + '</span>'
                );
            });

            previewContent.innerHTML = html;
        }

        // ================================================================
        // Existing-labels tree
        // ================================================================
        function renderExistingLabelsTree() {
            existingLabelsTree.innerHTML = '';
            existingCount.textContent = state.existingLabels.length;
            if (state.existingLabels.length === 0) {
                existingLabelsTree.innerHTML = '<div class="text-center py-5 text-muted"><p>No existing labels found.</p><p class="small mt-1">Use the AI Suggestions tab to add labels.</p></div>';
                return;
            }
            const groups = {};
            state.existingLabels.forEach(function(label) {
                const inner = label.current.replace(/^\{\{\s*|\s*\}\}$/g, '').replace(/^\{%\s*|\s*%\}$/g, '').trim();
                const base = inner.split('.')[0].split('[')[0];
                if (!groups[base]) groups[base] = [];
                groups[base].push(label);
            });
            Object.keys(groups).sort().forEach(function(groupName) {
                const groupDiv = document.createElement('div');
                groupDiv.className = 'mb-3';
                const header = document.createElement('div');
                header.className = 'tree-item d-flex align-items-center gap-2 px-2 py-1 rounded fw-medium';
                header.innerHTML = '<span class="tree-toggle">\u25BC</span><span>' + escapeHtml(groupName) + '</span><span class="badge bg-secondary">' + groups[groupName].length + '</span>';
                const children = document.createElement('div');
                children.className = 'tree-children mt-1';
                groups[groupName].forEach(function(label) {
                    const item = document.createElement('div');
                    item.className = 'existing-label p-2 rounded mb-1 cursor-pointer';
                    item.innerHTML = '<div class="font-monospace small break-all' + (label.original !== label.current ? ' text-primary' : '') + '">' + escapeHtml(label.current) + '</div>' + (label.original !== label.current ? '<div class="small text-muted mt-1 text-decoration-line-through">' + escapeHtml(label.original) + '</div>' : '') + '<div class="small text-muted mt-1">' + label.occurrences + ' occurrence' + (label.occurrences > 1 ? 's' : '') + '</div>';
                    item.addEventListener('click', function() { openEditLabelModal(label); });
                    children.appendChild(item);
                });
                header.addEventListener('click', function() {
                    var toggle = header.querySelector('.tree-toggle');
                    if (children.style.display === 'none') { children.style.display = 'block'; toggle.textContent = '\u25BC'; }
                    else { children.style.display = 'none'; toggle.textContent = '\u25B6'; }
                });
                groupDiv.appendChild(header);
                groupDiv.appendChild(children);
                existingLabelsTree.appendChild(groupDiv);
            });
            updateDownloadButton();
        }

        // ================================================================
        // Variable-picker tree
        // ================================================================
        function renderVariableTree(tree, prefix, container, filter, onSelectVariable) {
            tree = tree || getEffectiveVariableTree();
            prefix = prefix || '';
            container = container || variableTree;
            filter = filter || '';
            onSelectVariable = onSelectVariable || insertVariable;
            if (container === variableTree) container.innerHTML = '';
            Object.keys(tree).forEach(function(key) {
                if (key.startsWith('_')) return;
                var value = tree[key];
                var fullPath = prefix ? prefix + '.' + key : key;
                var displayPath = fullPath.replace(/\.\[/g, '[');
                if (filter && !displayPath.toLowerCase().includes(filter.toLowerCase())) {
                    if (typeof value !== 'object') return;
                    var hasMatch = JSON.stringify(value).toLowerCase().includes(filter.toLowerCase());
                    if (!hasMatch) return;
                }
                var item = document.createElement('div');
                item.className = 'mb-1';
                var isLeaf = typeof value === 'string' || (typeof value === 'object' && !!value._variable);
                var hasChildren = typeof value === 'object' && Object.keys(value).filter(function(k) { return !k.startsWith('_'); }).length > 0;
                var header = document.createElement('div');
                header.className = 'tree-item d-flex align-items-center gap-1 px-2 py-1 rounded small';
                if (isLeaf) {
                    var leafDescription = typeof value === 'string' ? value : (value._description || '');
                    header.innerHTML = '<span class="tree-toggle text-muted">\u00B7</span><span class="font-monospace text-primary cursor-pointer" data-var="' + displayPath + '">' + key + '</span><span class="text-muted small ms-1 text-truncate">' + leafDescription + '</span>';
                    header.querySelector('[data-var]').addEventListener('click', function() { onSelectVariable(displayPath); });
                } else {
                    header.innerHTML = '<span class="tree-toggle cursor-pointer">\u25B6</span><span class="fw-medium">' + key + '</span>' + (value._description ? '<span class="text-muted small ms-1">' + value._description + '</span>' : '');
                }
                item.appendChild(header);
                if (hasChildren) {
                    var children = document.createElement('div');
                    children.className = 'tree-children hidden';
                    renderVariableTree(value, fullPath, children, filter, onSelectVariable);
                    item.appendChild(children);
                    header.addEventListener('click', function(e) {
                        if (e.target.dataset && e.target.dataset.var) return;
                        var toggle = header.querySelector('.tree-toggle');
                        if (children.classList.contains('hidden')) { children.classList.remove('hidden'); toggle.textContent = '\u25BC'; }
                        else { children.classList.add('hidden'); toggle.textContent = '\u25B6'; }
                    });
                }
                container.appendChild(item);
            });
        }

        function insertVariable(varPath) {
            var input = document.getElementById('edit-label-input');
            if (input) { input.value = '{{ ' + varPath + ' }}'; input.focus(); }
        }

        // ================================================================
        // File processing
        // ================================================================
        async function processFile(file) {
            if (!file) return;
            state.file = file;
            updateModalSourceStatus();
            fileName.textContent = file.name;
            fileName.classList.remove('hidden');
            showLoading('Reading document...');
            try {
                var arrayBuffer = await file.arrayBuffer();
                state.fileContent = new Uint8Array(arrayBuffer);

                // Step 1: mammoth HTML preview
                showLoading('Converting document to HTML...');
                var result = await mammoth.convertToHtml({ arrayBuffer: arrayBuffer });
                state.originalHtml = result.value;
                previewContent.innerHTML = state.originalHtml;
                previewContent.classList.remove('hidden');
                previewEmpty.classList.add('hidden');

                // Step 2: extract runs via API (for preview mapping)
                showLoading('Extracting document structure...');
                var runsFormData = new FormData();
                runsFormData.append('file', file);
                runsFormData.append('defragment_runs', state.settings.defragmentRuns ? 'true' : 'false');
                try {
                    var runsResp = await fetch('/al/docx-labeler/api/extract-runs', { method: 'POST', body: runsFormData });
                    var runsData = await runsResp.json();
                    if (runsData.success) {
                        state.runs = runsData.data.runs;
                    }
                } catch (runErr) {
                    console.warn('Could not extract runs for preview mapping:', runErr);
                }

                state.existingLabels = extractExistingLabels(state.originalHtml);
                state.labelRenames = {};
                state.suggestions = [];
                state.validation = null;
                state.syntaxValidation = null;
                state.syntaxValidationPending = false;
                renderSyntaxValidation();

                hideLoading();
                showMainPanel();

                if (state.existingLabels.length > 0) {
                    switchTab('existing');
                    renderExistingLabelsTree();
                    updatePreview();
                } else {
                    switchTab('suggestions');
                    if (state.auth.aiEnabled) {
                        showLoading('Generating AI suggestions...');
                        await fetchSuggestions();
                    } else {
                        suggestionsList.innerHTML = '<div class="alert alert-warning">AI suggestions are unavailable until you log in.</div>';
                    }
                    hideLoading();
                    showMainPanel();    // <-- was missing; caused the "spin forever" bug
                }
            } catch (error) {
                console.error('Error processing file:', error);
                showError('Failed to process document: ' + error.message);
            } finally {
                hideLoading();
            }
        }

        // ================================================================
        // Tab switching
        // ================================================================
        function switchTab(tab) {
            state.activeTab = tab;
            tabExisting.classList.toggle('tab-active', tab === 'existing');
            tabManual.classList.toggle('tab-active', tab === 'manual');
            tabSuggestions.classList.toggle('tab-active', tab === 'suggestions');
            existingPanel.classList.toggle('hidden', tab !== 'existing');
            manualPanel.classList.toggle('hidden', tab !== 'manual');
            suggestionsPanel.classList.toggle('hidden', tab !== 'suggestions');
        }

        function renderManualLabels() {
            var manualLabels = getManualLabelGroups();
            var hasManualLabels = manualLabels.length > 0;
            tabManual.classList.toggle('hidden', !hasManualLabels);
            manualCount.textContent = manualLabels.length;

            if (!hasManualLabels) {
                manualLabelsList.innerHTML = '<div class="text-center py-5 text-muted"><p>No manual labels yet.</p><p class="small mt-1">Select text in the preview to add one.</p></div>';
                if (state.activeTab === 'manual') {
                    switchTab(state.existingLabels.length > 0 ? 'existing' : 'suggestions');
                }
                return;
            }

            manualLabelsList.innerHTML = '';
            manualLabels.forEach(function(label) {
                var div = document.createElement('div');
                div.className = 'p-3 rounded border border-2 mb-2 suggestion-accepted';
                div.innerHTML =
                    '<div class="d-flex align-items-start justify-content-between gap-2 mb-2">' +
                        '<span class="small text-muted">Selection</span>' +
                        '<button class="manual-remove-btn btn btn-sm btn-outline-danger" data-group="' + escapeHtml(label.group) + '" title="Remove">\u2716</button>' +
                    '</div>' +
                    '<div class="small text-muted mb-2">\u201C' + escapeHtml(label._selectedText || '') + '\u201D</div>' +
                    '<label class="form-label small fw-medium mb-1">Label</label>' +
                    '<input type="text" class="manual-edit-input form-control form-control-sm font-monospace mb-2" value="' + escapeHtml(label._displayLabel || label.text || '') + '" data-group="' + escapeHtml(label.group) + '">' +
                    '<div class="small text-muted">Para ' + escapeHtml(String(label.paragraph)) + ', Run ' + escapeHtml(String(label.run)) + '</div>';
                manualLabelsList.appendChild(div);
            });

            manualLabelsList.querySelectorAll('.manual-edit-input').forEach(function(input) {
                input.addEventListener('input', function() {
                    var value = input.value.trim();
                    if (!value) return;
                    applyManualLabelText(input.dataset.group, value);
                    updatePreview();
                    scheduleSyntaxValidation(500);
                });
            });

            manualLabelsList.querySelectorAll('.manual-remove-btn').forEach(function(button) {
                button.addEventListener('click', function() {
                    var groupId = button.dataset.group;
                    state.suggestions = state.suggestions.filter(function(suggestion) {
                        return suggestion.group !== groupId;
                    });
                    renderSuggestions();
                    updatePreview();
                    updateDownloadButton();
                    scheduleSyntaxValidation();
                });
            });
        }

        // ================================================================
        // Edit label modal
        // ================================================================
        function openEditLabelModal(label) {
            state.editingLabelId = label.id;
            document.getElementById('edit-label-original').textContent = label.original;
            document.getElementById('edit-label-input').value = label.current;
            document.getElementById('edit-label-title').textContent = 'Edit Label';
            document.getElementById('variable-search').value = '';
            renderVariableTree(getEffectiveVariableTree());
            editLabelModal.classList.remove('hidden');
            document.getElementById('edit-label-input').focus();
        }

        function saveEditLabel() {
            var input = document.getElementById('edit-label-input').value.trim();
            if (!input || !state.editingLabelId) return;
            var label = state.existingLabels.find(function(l) { return l.id === state.editingLabelId; });
            if (label) { label.current = input; state.labelRenames[label.original] = input; }
            editLabelModal.classList.add('hidden');
            state.editingLabelId = null;
            renderExistingLabelsTree();
            updatePreview();
            scheduleSyntaxValidation();
        }

        // ================================================================
        // Bulk find & replace
        // ================================================================
        function previewBulkReplace() {
            var find = document.getElementById('bulk-find').value;
            var replace = document.getElementById('bulk-replace').value;
            if (!find) { document.getElementById('bulk-preview').classList.add('hidden'); return; }
            try {
                var regex = new RegExp(find, 'g');
                var previewList = document.getElementById('bulk-preview-list');
                previewList.innerHTML = '';
                var matchCount = 0;
                state.existingLabels.forEach(function(label) {
                    var inner = label.current.replace(/^\{\{\s*|\s*\}\}$/g, '');
                    if (regex.test(inner)) {
                        regex.lastIndex = 0;
                        var newInner = inner.replace(regex, replace);
                        var newLabel = '{{ ' + newInner + ' }}';
                        var div = document.createElement('div');
                        div.className = 'font-monospace';
                        div.innerHTML = '<span class="text-danger text-decoration-line-through">' + escapeHtml(label.current) + '</span><span class="text-muted mx-1">\u2192</span><span class="text-success">' + escapeHtml(newLabel) + '</span>';
                        previewList.appendChild(div);
                        matchCount++;
                    }
                });
                if (matchCount > 0) { document.getElementById('bulk-preview').classList.remove('hidden'); }
                else { previewList.innerHTML = '<span class="text-muted">No matches found</span>'; document.getElementById('bulk-preview').classList.remove('hidden'); }
            } catch (e) {
                document.getElementById('bulk-preview-list').innerHTML = '<span class="text-danger">Invalid regex: ' + e.message + '</span>';
                document.getElementById('bulk-preview').classList.remove('hidden');
            }
        }

        function applyBulkReplace() {
            var find = document.getElementById('bulk-find').value;
            var replace = document.getElementById('bulk-replace').value;
            if (!find) return;
            try {
                var regex = new RegExp(find, 'g');
                state.existingLabels.forEach(function(label) {
                    var inner = label.current.replace(/^\{\{\s*|\s*\}\}$/g, '');
                    if (regex.test(inner)) {
                        regex.lastIndex = 0;
                        var newInner = inner.replace(regex, replace);
                        label.current = '{{ ' + newInner + ' }}';
                        state.labelRenames[label.original] = label.current;
                    }
                });
                bulkReplaceModal.classList.add('hidden');
                renderExistingLabelsTree();
                updatePreview();
                scheduleSyntaxValidation();
            } catch (e) { alert('Invalid regex: ' + e.message); }
        }

        // ================================================================
        // AI suggestions
        // ================================================================
        async function fetchSuggestions() {
            if (!state.auth.aiEnabled) {
                suggestionsList.innerHTML = '<div class="alert alert-warning">AI suggestions require login.</div>';
                return;
            }
            var selectedSourceState = getActiveInterviewSourceState();
            if (state.settings.usePlaygroundVariables && !selectedSourceState.selectedFile) {
                suggestionsList.innerHTML = '<div class="alert alert-warning">Select an interview before using interview variable names.</div>';
                return;
            }
            var formData = new FormData();
            formData.append('file', state.file);
            formData.append('defragment_runs', state.settings.defragmentRuns ? 'true' : 'false');
            if (state.settings.additionalInstructions) formData.append('additional_instructions', state.settings.additionalInstructions);
            if (state.settings.contextText) formData.append('context_text', state.settings.contextText);
            if (state.settings.customPeople) formData.append('custom_people_names', state.settings.customPeople);
            formData.append('prompt_profile', state.settings.promptProfile || 'standard');
            formData.append('model', state.settings.model);
            if (state.settings.judgeModel) formData.append('judge_model', state.settings.judgeModel);
            if (state.settings.usePlaygroundVariables) {
                if (!selectedSourceState.variables.length) {
                    suggestionsList.innerHTML = '<div class="alert alert-warning">No interview variables were found for the selected interview.</div>';
                    return;
                }
                formData.append('use_playground_variables', 'true');
                formData.append('interview_source_mode', state.interviewSourceMode);
                formData.append('preferred_variable_names', JSON.stringify(selectedSourceState.variables));
                if (state.interviewSourceMode === 'installed') {
                    formData.append('installed_package', state.installed.selectedPackage);
                    formData.append('installed_yaml_file', state.installed.selectedFile);
                    formData.append('installed_interview_path', state.installed.selectedPackage + ':' + state.installed.selectedFile);
                } else {
                    formData.append('playground_project', state.playground.selectedProject);
                    formData.append('playground_yaml_file', state.playground.selectedFile);
                }
            }
            if (state.settings.generationMethod === 'single') {
                formData.append('generator_models', JSON.stringify([state.settings.model]));
            } else if (state.settings.generationMethod === 'multi_model') {
                var generatorModels = parseGeneratorModelsInput(state.settings.generatorModels);
                if (generatorModels.length === 0) generatorModels = [state.settings.model];
                formData.append('generator_models', JSON.stringify(generatorModels));
            }
            try {
                var manualSuggestions = state.suggestions.filter(function(suggestion) {
                    return isManualSuggestion(suggestion);
                });
                var data = await requestAsyncLabelerAction(
                    '/al/docx-labeler/api/suggest-labels',
                    formData,
                    'AI labeling is running in the background...'
                );
                state.suggestions = manualSuggestions.concat((data.suggestions || []).map(function(s) {
                    return Object.assign({}, s, { id: s.id || generateId(), status: 'pending' });
                }));
                state.validation = data.validation || null;
                suggestionsCount.textContent = state.suggestions.length;
                renderSuggestions();
            } catch (error) {
                console.error('Error fetching suggestions:', error);
                state.validation = null;
                suggestionsList.innerHTML = '<div class="alert alert-danger">' + escapeHtml(error.message) + '</div>';
            }
        }

        function renderSuggestions() {
            suggestionsList.innerHTML = '';
            renderManualLabels();
            var deterministic = state.validation && state.validation.deterministic ? state.validation.deterministic : null;
            var syntaxValidation = deterministic && deterministic.syntax_validation ? deterministic.syntax_validation : null;
            var documentWarnings = state.validation && Array.isArray(state.validation.document_warnings) ? state.validation.document_warnings : [];
            var aggregation = state.validation && state.validation.aggregation ? state.validation.aggregation : null;
            var timings = state.validation && state.validation.timings ? state.validation.timings : null;
            var aiSuggestions = state.suggestions.filter(function(suggestion) { return !isManualSuggestion(suggestion); });
            if ((deterministic && deterministic.flagged_count > 0) || documentWarnings.length > 0 || aggregation || (syntaxValidation && ((syntaxValidation.error_count || 0) > 0 || (syntaxValidation.warning_count || 0) > 0))) {
                var aiReview = state.validation && state.validation.ai_review ? state.validation.ai_review : {};
                var reviewedCount = Array.isArray(aiReview.reviews) ? aiReview.reviews.length : 0;
                suggestionsValidation.classList.remove('hidden');
                var summaryHtml = '';
                if (aggregation) {
                    var confidenceCounts = aggregation.confidence_counts || {};
                    summaryHtml +=
                        '<div><strong>Aggregated ' + aiSuggestions.length + '</strong> ranked suggestion' + (aiSuggestions.length === 1 ? '' : 's') +
                        ' from ' + escapeHtml(String(aggregation.generator_runs || 0)) + ' generation run' + ((aggregation.generator_runs || 0) === 1 ? '' : 's') + '.</div>' +
                        '<div class="mt-1">Confidence tiers: ' +
                        '<span class="badge bg-success-subtle text-success-emphasis border border-success-subtle me-1">High ' + escapeHtml(String(confidenceCounts.high || 0)) + '</span>' +
                        '<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle me-1">Medium ' + escapeHtml(String(confidenceCounts.medium || 0)) + '</span>' +
                        '<span class="badge bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle">Low ' + escapeHtml(String(confidenceCounts.low || 0)) + '</span>' +
                        '</div>';
                    if (aggregation.ambiguous_group_count) {
                        summaryHtml += '<div class="mt-1">Judge reviewed ' + escapeHtml(String(reviewedCount)) + ' ambiguous group' + (reviewedCount === 1 ? '' : 's') + '.</div>';
                    }
                }
                if (deterministic && deterministic.flagged_count > 0) {
                    summaryHtml +=
                        '<div class="mt-1"><strong>Validator flagged ' + deterministic.flagged_count + '</strong> selected suggestion' + (deterministic.flagged_count === 1 ? '' : 's') + '.</div>';
                }
                if (syntaxValidation && (syntaxValidation.error_count || 0) > 0) {
                    summaryHtml += '<div class="mt-1"><strong>Final template syntax errors:</strong></div>';
                    syntaxValidation.errors.forEach(function(issue) {
                        summaryHtml += '<div>' + escapeHtml(issue.message || issue.code || 'Syntax error') + '</div>';
                    });
                } else if (syntaxValidation && (syntaxValidation.warning_count || 0) > 0) {
                    summaryHtml += '<div class="mt-1"><strong>Final template warnings:</strong></div>';
                    syntaxValidation.warnings.forEach(function(issue) {
                        summaryHtml += '<div>' + escapeHtml(issue.message || issue.code || 'Warning') + '</div>';
                    });
                }
                if (documentWarnings.length > 0) {
                    summaryHtml += '<div class="mt-1"><strong>Source document warnings:</strong></div>';
                    documentWarnings.forEach(function(warning) {
                        summaryHtml += '<div>' + escapeHtml(warning.message || warning.code || 'Document warning') + '</div>';
                    });
                }                
                suggestionsValidation.innerHTML = summaryHtml;
            } else {
                suggestionsValidation.classList.add('hidden');
                suggestionsValidation.textContent = '';
            }
            var hiddenLowConfidenceCount = 0;
            // Count only visible (non-companion) suggestions
            var visibleCount = aiSuggestions.filter(function(s) {
                if (s._isCompanion) return false;
                if (!state.settings.showLowConfidence && (s.confidence || 'low') === 'low') {
                    hiddenLowConfidenceCount += 1;
                    return false;
                }
                return true;
            }).length;
            suggestionsCount.textContent = visibleCount;
            lowConfidenceSummary.textContent = hiddenLowConfidenceCount > 0
                ? hiddenLowConfidenceCount + ' low confidence suggestion' + (hiddenLowConfidenceCount === 1 ? '' : 's') + ' hidden'
                : '';
            if (visibleCount === 0) {
                suggestionsList.innerHTML = hiddenLowConfidenceCount > 0
                    ? '<p class="text-muted text-center py-5">Only low confidence suggestions are available. Turn on “Show low confidence” to review them.</p>'
                    : '<p class="text-muted text-center py-5">No suggestions available.</p>';
                return;
            }
            state.suggestions.forEach(function(suggestion, index) {
                // Skip companion entries – they’re part of a multi-run group
                if (suggestion._isCompanion) return;
                if (isManualSuggestion(suggestion)) return;
                if (!state.settings.showLowConfidence && (suggestion.confidence || 'low') === 'low') return;

                var div = document.createElement('div');
                var stateClass = suggestion.status === 'accepted' ? 'suggestion-accepted' : suggestion.status === 'rejected' ? 'suggestion-rejected' : 'suggestion-pending';
                div.className = 'p-3 rounded border border-2 mb-2 ' + stateClass;

                var locationLabel = suggestion.group
                    ? '<span class="small text-muted">Selection \u2192 ' + escapeHtml(suggestion._displayLabel || suggestion.text) + '</span>'
                    : '<span class="small text-muted">Para ' + suggestion.paragraph + ', Run ' + suggestion.run + '</span>';
                var validationFlags = Array.isArray(suggestion.validation_flags) ? suggestion.validation_flags : [];
                var judgeReview = suggestion.judge_review || null;
                var confidenceMeta = getConfidenceMeta(suggestion.confidence || 'low');
                var supportHtml = '';
                if (!suggestion.group && suggestion.vote_total) {
                    supportHtml =
                        '<span class="badge ' + confidenceMeta.badge + '">' + escapeHtml(confidenceMeta.label) + '</span>' +
                        '<span class="badge text-bg-light border ms-1">' + escapeHtml(String(suggestion.clean_vote_count || 0)) + '/' + escapeHtml(String(suggestion.vote_total || 0)) + ' clean votes</span>';
                    var sourceSummary = summarizeSuggestionSources(suggestion);
                    if (sourceSummary) {
                        supportHtml += '<div class="small text-muted mt-1">Sources: ' + escapeHtml(sourceSummary) + '</div>';
                    }
                }
                var reviewHtml = '';
                if (validationFlags.length > 0) {
                    reviewHtml += '<div class="alert alert-warning small mt-2 mb-2 py-2">';
                    reviewHtml += '<div class="fw-semibold mb-1">Validator flags</div>';
                    validationFlags.forEach(function(flag) {
                        reviewHtml += '<div>' + escapeHtml(flag.message || flag.code || 'Flagged') + '</div>';
                    });
                    if (judgeReview && judgeReview.reason) {
                        reviewHtml += '<div class="mt-2"><span class="fw-semibold">Judge:</span> ' + escapeHtml(judgeReview.reason) + '</div>';
                    }
                    reviewHtml += '</div>';
                } else if (judgeReview && judgeReview.reason) {
                    reviewHtml += '<div class="small text-muted mt-2">Judge note: ' + escapeHtml(judgeReview.reason) + '</div>';
                }

                var alternates = Array.isArray(suggestion.alternates) ? suggestion.alternates : [];
                var alternatesHtml = '';
                if (!suggestion.group && alternates.length > 0) {
                    alternatesHtml += '<details class="mt-2"><summary class="small fw-semibold">Alternates (' + alternates.length + ')</summary>';
                    alternates.forEach(function(alternate, altIndex) {
                        var alternateMeta = getConfidenceMeta(alternate.confidence || 'low');
                        var alternateFlags = Array.isArray(alternate.validation_flags) ? alternate.validation_flags : [];
                        alternatesHtml +=
                            '<div class="border rounded p-2 mt-2 bg-light-subtle">' +
                                '<div class="d-flex justify-content-between align-items-start gap-2">' +
                                    '<div class="font-monospace small break-all flex-grow-1">' + escapeHtml(alternate.text || '') + '</div>' +
                                    '<button type="button" class="btn btn-sm btn-outline-primary use-alternate-btn" data-index="' + index + '" data-alt-index="' + altIndex + '">Use</button>' +
                                '</div>' +
                                '<div class="mt-2">' +
                                    '<span class="badge ' + alternateMeta.badge + '">' + escapeHtml(alternateMeta.label) + '</span>' +
                                    '<span class="badge text-bg-light border ms-1">' + escapeHtml(String(alternate.clean_vote_count || 0)) + '/' + escapeHtml(String(alternate.vote_total || suggestion.vote_total || 0)) + ' clean votes</span>' +
                                '</div>';
                        if (alternateFlags.length > 0) {
                            alternatesHtml += '<div class="small text-warning-emphasis mt-1">';
                            alternateFlags.forEach(function(flag) {
                                alternatesHtml += '<div>' + escapeHtml(flag.message || flag.code || 'Flagged') + '</div>';
                            });
                            alternatesHtml += '</div>';
                        }
                        alternatesHtml += '</div>';
                    });
                    alternatesHtml += '</details>';
                }

                div.innerHTML =
                    '<div class="d-flex align-items-start justify-content-between gap-2 mb-2">' +
                        locationLabel +
                        '<div class="d-flex gap-1">' +
                            '<button class="accept-btn btn btn-sm ' + (suggestion.status === 'accepted' ? 'btn-success' : 'btn-outline-secondary') + '" data-index="' + index + '">\u2713</button>' +
                            '<button class="reject-btn btn btn-sm ' + (suggestion.status === 'rejected' ? 'btn-danger' : 'btn-outline-secondary') + '" data-index="' + index + '">\u2717</button>' +
                            (suggestion.group ? '<button class="remove-btn btn btn-sm btn-outline-danger" data-index="' + index + '" title="Remove">\u2716</button>' : '') +
                        '</div>' +
                    '</div>' +
                    (suggestion._selectedText ? '<div class="small text-muted mb-1">\u201C' + escapeHtml(suggestion._selectedText) + '\u201D</div>' : '') +
                    (supportHtml ? '<div class="mb-2">' + supportHtml + '</div>' : '') +
                    '<div class="font-monospace small break-all">' + escapeHtml(suggestion._displayLabel || suggestion.text) + '</div>' +
                    reviewHtml +
                    alternatesHtml +
                    (suggestion.group ? '' : '<input type="text" class="edit-input form-control form-control-sm font-monospace mt-2' + (suggestion.status === 'rejected' ? ' hidden' : '') + '" value="' + escapeHtml(suggestion.text) + '" data-index="' + index + '">');
                suggestionsList.appendChild(div);
            });
            suggestionsList.querySelectorAll('.accept-btn').forEach(function(btn) {
                btn.addEventListener('click', function() {
                    var index = parseInt(btn.dataset.index);
                    var s = state.suggestions[index];
                    var newStatus = s.status === 'accepted' ? 'pending' : 'accepted';
                    s.status = newStatus;
                    // Propagate to companions in same group
                    if (s.group) {
                        state.suggestions.forEach(function(c) {
                            if (c.group === s.group) c.status = newStatus;
                        });
                    }
                    renderSuggestions();
                    updatePreview();
                    updateDownloadButton();
                    scheduleSyntaxValidation();
                });
            });
            suggestionsList.querySelectorAll('.reject-btn').forEach(function(btn) {
                btn.addEventListener('click', function() {
                    var index = parseInt(btn.dataset.index);
                    var s = state.suggestions[index];
                    var newStatus = s.status === 'rejected' ? 'pending' : 'rejected';
                    s.status = newStatus;
                    if (s.group) {
                        state.suggestions.forEach(function(c) {
                            if (c.group === s.group) c.status = newStatus;
                        });
                    }
                    renderSuggestions();
                    updatePreview();
                    updateDownloadButton();
                    scheduleSyntaxValidation();
                });
            });
            // Remove button deletes grouped selection labels entirely
            suggestionsList.querySelectorAll('.remove-btn').forEach(function(btn) {
                btn.addEventListener('click', function() {
                    var index = parseInt(btn.dataset.index);
                    var grp = state.suggestions[index].group;
                    if (grp) {
                        state.suggestions = state.suggestions.filter(function(c) { return c.group !== grp; });
                    } else {
                        state.suggestions.splice(index, 1);
                    }
                    renderSuggestions();
                    updatePreview();
                    updateDownloadButton();
                    scheduleSyntaxValidation();
                });
            });
            suggestionsList.querySelectorAll('.edit-input').forEach(function(input) {
                input.addEventListener('input', function() {
                    var index = parseInt(input.dataset.index);
                    state.suggestions[index].text = input.value;
                    updatePreview();
                    scheduleSyntaxValidation(500);
                });
            });
            suggestionsList.querySelectorAll('.use-alternate-btn').forEach(function(btn) {
                btn.addEventListener('click', function() {
                    var index = parseInt(btn.dataset.index);
                    var altIndex = parseInt(btn.dataset.altIndex);
                    swapSuggestionWithAlternate(state.suggestions[index], altIndex);
                    renderSuggestions();
                    updatePreview();
                    updateDownloadButton();
                    scheduleSyntaxValidation();
                });
            });
            updateDownloadButton();
        }

        // ================================================================
        // Playground open / save
        // ================================================================
        async function openDocxPlaygroundModal() {
            if (!state.auth.isAuthenticated) { showError('Login required to access Playground.'); return; }
            // Show modal immediately with loading states
            var openPgProject = document.getElementById('open-pg-project');
            var openPgTemplate = document.getElementById('open-pg-template');
            openPgProject.innerHTML = '<option value="">Loading projects...</option>';
            openPgTemplate.innerHTML = '<option value="">Waiting for projects...</option>';
            openPlaygroundModalEl.classList.remove('hidden');
            // Load data in background
            (async function() {
                try {
                    await fetchPlaygroundProjects();
                    openPgProject.innerHTML = '';
                    state.playground.projects.forEach(function(proj) {
                        var opt = document.createElement('option');
                        opt.value = proj; opt.textContent = proj;
                        opt.selected = proj === state.playground.selectedProject;
                        openPgProject.appendChild(opt);
                    });
                    await fetchDocxOpenPlaygroundTemplates();
                } catch (e) {
                    console.error('Error loading playground data:', e);
                    openPgProject.innerHTML = '<option value="">(error loading projects)</option>';
                    openPgTemplate.innerHTML = '<option value="">(error loading templates)</option>';
                }
            })();
        }

        async function fetchDocxOpenPlaygroundTemplates() {
            var openPgProject = document.getElementById('open-pg-project');
            var openPgTemplate = document.getElementById('open-pg-template');
            var project = openPgProject.value || 'default';
            openPgTemplate.innerHTML = '<option value="">Loading...</option>';
            try {
                var data = await fetchJsonOrThrow(
                    '/al/labeler/api/playground-templates?project=' + encodeURIComponent(project) + '&type=docx'
                );
                var templates = data && data.success && data.data && Array.isArray(data.data.templates) ? data.data.templates : [];
                openPgTemplate.innerHTML = '';
                if (templates.length === 0) {
                    var opt = document.createElement('option');
                    opt.value = ''; opt.textContent = '(no DOCX templates found)';
                    openPgTemplate.appendChild(opt);
                    return;
                }
                templates.forEach(function(t) {
                    var opt = document.createElement('option');
                    opt.value = t.filename; opt.textContent = t.filename + (t.size ? ' (' + Math.round(t.size / 1024) + ' KB)' : '');
                    openPgTemplate.appendChild(opt);
                });
            } catch (_e) {
                openPgTemplate.innerHTML = '<option value="">(error loading templates)</option>';
            }
        }

        async function confirmOpenFromPlayground() {
            var openPgProject = document.getElementById('open-pg-project');
            var openPgTemplate = document.getElementById('open-pg-template');
            var project = openPgProject.value || 'default';
            var filename = openPgTemplate.value;
            if (!filename) { showError('Select a template file.'); return; }
            openPlaygroundModalEl.classList.add('hidden');
            showLoading('Loading ' + filename + ' from Playground...');
            try {
                var data = await fetchJsonOrThrow(
                    '/al/labeler/api/playground-templates/load?project=' + encodeURIComponent(project) + '&filename=' + encodeURIComponent(filename)
                );
                if (!data.success || !data.data || !data.data.file_content_base64) {
                    throw new Error((data.error && data.error.message) || 'Failed to load template.');
                }
                var binaryString = atob(data.data.file_content_base64);
                var bytes = new Uint8Array(binaryString.length);
                for (var i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
                var blob = new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
                var file = new File([blob], filename, { type: blob.type });
                state.playgroundSource = { project: project, filename: filename };
                savePlaygroundBtn.classList.remove('hidden');
                await processFile(file);
            } catch (error) {
                showError('Failed to load from Playground: ' + (error.message || 'Unknown error.'));
            } finally { hideLoading(); }
        }

        async function openSavePlaygroundModal() {
            if (!state.auth.isAuthenticated) { showError('Login required to save to Playground.'); return; }
            if (!state.file) { showError('No DOCX file loaded.'); return; }
            // Show modal immediately
            var savePgProject = document.getElementById('save-pg-project');
            var savePgFilename = document.getElementById('save-pg-filename');
            var savePgStatus = document.getElementById('save-pg-status');
            savePgProject.innerHTML = '<option value="">Loading projects...</option>';
            savePgFilename.value = (state.playgroundSource && state.playgroundSource.filename) || (state.file && state.file.name) || 'template.docx';
            if (!savePgFilename.value.toLowerCase().endsWith('.docx'))
                savePgFilename.value += '.docx';
            savePgStatus.classList.add('hidden');
            savePlaygroundModalEl.classList.remove('hidden');
            // Load projects in background
            (async function() {
                try {
                    await fetchPlaygroundProjects();
                    savePgProject.innerHTML = '';
                    state.playground.projects.forEach(function(proj) {
                        var opt = document.createElement('option');
                        opt.value = proj; opt.textContent = proj;
                        opt.selected = proj === ((state.playgroundSource && state.playgroundSource.project) || state.playground.selectedProject);
                        savePgProject.appendChild(opt);
                    });
                } catch (e) {
                    console.error('Error loading projects:', e);
                    savePgProject.innerHTML = '<option value="">(error loading projects)</option>';
                }
            })();
        }

        async function confirmSaveToPlayground() {
            var savePgProject = document.getElementById('save-pg-project');
            var savePgFilename = document.getElementById('save-pg-filename');
            var savePgStatus = document.getElementById('save-pg-status');
            var project = savePgProject.value || 'default';
            var filename = savePgFilename.value.trim();
            if (!filename || !filename.toLowerCase().endsWith('.docx')) {
                savePgStatus.textContent = 'Filename must end with .docx'; savePgStatus.classList.remove('hidden'); return;
            }
            savePlaygroundModalEl.classList.add('hidden');
            showLoading('Saving to Playground...');
            try {
                var syntaxCheck = await ensureSyntaxValidationBeforeWrite('saving to Playground');
                if (!syntaxCheck.allowed) {
                    hideLoading();
                    return;
                }
                // First apply any pending changes via the apply-labels endpoint
                var renames = collectRenamePayload();
                var acceptedLabels = collectAcceptedLabels();

                var docxBase64;
                var applyHighlights = acceptedLabels.length > 0;
                if (renames.length > 0 || acceptedLabels.length > 0) {
                    var formData = new FormData();
                    formData.append('file', state.file);
                    formData.append('defragment_runs', state.settings.defragmentRuns ? 'true' : 'false');
                    if (renames.length > 0) formData.append('renames', JSON.stringify(renames));
                    if (acceptedLabels.length > 0) formData.append('labels', JSON.stringify(acceptedLabels));
                    if (applyHighlights) formData.append('apply_highlights', 'true');
                    if (syntaxCheck.allowInvalidSyntax) formData.append('allow_invalid_syntax', 'true');
                    var applyResponse = await fetch('/al/docx-labeler/api/apply-labels', { method: 'POST', body: formData });
                    var applyData = await parseApiResponse(applyResponse);
                    docxBase64 = applyData.data.docx_base64;
                } else {
                    // No changes - send the original file as base64
                    var arrayBuffer = await state.file.arrayBuffer();
                    var bytes = new Uint8Array(arrayBuffer);
                    var raw = '';
                    for (var i = 0; i < bytes.length; i++) raw += String.fromCharCode(bytes[i]);
                    docxBase64 = window.btoa(raw);
                }

                var response = await fetch('/al/labeler/api/playground-templates/save', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
                    body: JSON.stringify({ project: project, filename: filename, file_content_base64: docxBase64 }),
                    credentials: 'same-origin'
                });
                var data = await response.json();
                if (!data.success) throw new Error((data.error && data.error.message) || 'Save failed.');
                state.playgroundSource = { project: project, filename: filename };
                savePlaygroundBtn.classList.remove('hidden');
                downloadStatus.textContent = (data.data && data.data.created ? 'Created' : 'Updated') + ' ' + filename + ' in Playground.';
                downloadStatus.classList.remove('hidden');
            } catch (error) {
                showError('Save to Playground failed: ' + (error.message || 'Unknown error.'));
            } finally { hideLoading(); }
        }

        // ================================================================
        // Download
        // ================================================================
        function updateDownloadButton() {
            var hasChanges = collectRenamePayload().length > 0 || collectAcceptedLabels().length > 0;
            downloadBtn.disabled = !hasChanges;
            applyHighlightsBtn.disabled = !state.file;
            // Save to Playground is enabled when a file is loaded (changes or not, the user may want to save the original)
            savePlaygroundBtn.disabled = !state.file;
        }

        async function downloadDocument(options) {
            options = options || {};
            if (!state.file) return;
            var renames = collectRenamePayload();
            var acceptedLabels = collectAcceptedLabels();
            var applyHighlights = !!options.forceHighlights || acceptedLabels.length > 0;
            if (renames.length === 0 && acceptedLabels.length === 0 && !applyHighlights) { alert('No changes to apply.'); return; }
            downloadStatus.textContent = 'Processing...';
            downloadStatus.classList.remove('hidden');
            downloadBtn.disabled = true;
            applyHighlightsBtn.disabled = true;
            try {
                var syntaxCheck = await ensureSyntaxValidationBeforeWrite('downloading this DOCX');
                if (!syntaxCheck.allowed) {
                    downloadStatus.classList.add('hidden');
                    return;
                }
                var formData = new FormData();
                formData.append('file', state.file);
                formData.append('defragment_runs', state.settings.defragmentRuns ? 'true' : 'false');
                if (renames.length > 0) formData.append('renames', JSON.stringify(renames));
                if (acceptedLabels.length > 0) formData.append('labels', JSON.stringify(acceptedLabels));
                if (applyHighlights) formData.append('apply_highlights', 'true');
                if (syntaxCheck.allowInvalidSyntax) formData.append('allow_invalid_syntax', 'true');
                var response = await fetch('/al/docx-labeler/api/apply-labels', { method: 'POST', body: formData });
                var data = await parseApiResponse(response);
                var binaryString = atob(data.data.docx_base64);
                var bytes = new Uint8Array(binaryString.length);
                for (var i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
                var blob = new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
                var url = URL.createObjectURL(blob);
                var a = document.createElement('a');
                a.href = url;
                a.download = data.data.filename;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
                if (data.data.defragmented_before_apply && data.data.defragmentation) {
                    var paragraphCount = data.data.defragmentation.paragraphs_defragmented || 0;
                    var runsRemoved = data.data.defragmentation.runs_removed || 0;
                    downloadStatus.textContent = 'Download complete. Consolidated fragmented runs in ' + paragraphCount + ' paragraph' + (paragraphCount === 1 ? '' : 's') + ' before applying labels (' + runsRemoved + ' runs merged).';
                } else if (data.data.apply_highlights) {
                    downloadStatus.textContent = 'Download complete with Jinja highlights.';
                } else {
                    downloadStatus.textContent = 'Download complete!';
                    setTimeout(function() { downloadStatus.classList.add('hidden'); }, 2000);
                }
            } catch (error) {
                console.error('Error downloading:', error);
                downloadStatus.textContent = 'Error: ' + error.message;
            } finally { updateDownloadButton(); }
        }

        // ================================================================
        // Event listeners
        // ================================================================
        fileInput.addEventListener('change', function(e) { var file = e.target.files[0]; if (file) processFile(file); });

        // Playground open / save listeners
        openPlaygroundBtn.addEventListener('click', openDocxPlaygroundModal);
        savePlaygroundBtn.addEventListener('click', openSavePlaygroundModal);
        document.getElementById('open-pg-project').addEventListener('change', fetchDocxOpenPlaygroundTemplates);
        document.getElementById('open-pg-confirm').addEventListener('click', confirmOpenFromPlayground);
        document.getElementById('open-pg-cancel').addEventListener('click', function() { openPlaygroundModalEl.classList.add('hidden'); });
        document.getElementById('close-open-playground').addEventListener('click', function() { openPlaygroundModalEl.classList.add('hidden'); });
        document.getElementById('save-pg-confirm').addEventListener('click', confirmSaveToPlayground);
        document.getElementById('save-pg-cancel').addEventListener('click', function() { savePlaygroundModalEl.classList.add('hidden'); });
        document.getElementById('close-save-playground').addEventListener('click', function() { savePlaygroundModalEl.classList.add('hidden'); });
        // Close playground modals on backdrop click
        [openPlaygroundModalEl, savePlaygroundModalEl].forEach(function(modal) {
            modal.addEventListener('click', function(e) { if (e.target === modal) modal.classList.add('hidden'); });
        });
        utilitiesBtn.addEventListener('click', function() {
            updateModalSourceStatus();
            utilitiesResult.innerHTML = '';
            utilitiesModal.classList.remove('hidden');
        });
        repairBtn.addEventListener('click', function() {
            updateModalSourceStatus();
            repairResult.innerHTML = '';
            repairModal.classList.remove('hidden');
        });
        document.getElementById('utilities-close').addEventListener('click', function() { utilitiesModal.classList.add('hidden'); });
        document.getElementById('repair-close').addEventListener('click', function() { repairModal.classList.add('hidden'); });
        [utilitySourceActive, utilitySourceUpload, repairSourceActive, repairSourceUpload].forEach(function(input) {
            input.addEventListener('change', updateModalSourceStatus);
        });
        utilityFileInput.addEventListener('change', updateModalSourceStatus);
        repairFileInput.addEventListener('change', updateModalSourceStatus);
        document.querySelectorAll('.utility-action-btn').forEach(function(button) {
            button.addEventListener('click', function() {
                runDocxOperation('utility', button.dataset.action, 'Running DOCX utility...');
            });
        });
        document.querySelectorAll('.repair-action-btn').forEach(function(button) {
            button.addEventListener('click', function() {
                runDocxOperation('repair', button.dataset.action, 'Running DOCX repair...');
            });
        });
        tabExisting.addEventListener('click', function() { switchTab('existing'); });
        tabManual.addEventListener('click', function() { switchTab('manual'); });
        tabSuggestions.addEventListener('click', function() { switchTab('suggestions'); });
        document.getElementById('accept-all-btn').addEventListener('click', function() {
            state.suggestions.forEach(function(s) { if (s.status === 'pending') s.status = 'accepted'; });
            renderSuggestions();
            updatePreview();
        });
        document.getElementById('regenerate-btn').addEventListener('click', async function() {
            if (!state.auth.aiEnabled) {
                showError('AI suggestions require login.');
                return;
            }
            if (state.file) { showLoading('Regenerating suggestions...'); await fetchSuggestions(); hideLoading(); showMainPanel(); }
        });
        document.getElementById('bulk-replace-btn').addEventListener('click', function() {
            document.getElementById('bulk-find').value = '';
            document.getElementById('bulk-replace').value = '';
            document.getElementById('bulk-preview').classList.add('hidden');
            bulkReplaceModal.classList.remove('hidden');
        });
        document.getElementById('ai-relabel-btn').addEventListener('click', function() {
            alert('AI Relabeling coming soon! This will use AI to suggest better AssemblyLine-compatible names for your existing labels.');
        });
        document.getElementById('bulk-preview-btn').addEventListener('click', previewBulkReplace);
        document.getElementById('bulk-apply').addEventListener('click', applyBulkReplace);
        document.getElementById('bulk-cancel').addEventListener('click', function() { bulkReplaceModal.classList.add('hidden'); });
        document.getElementById('edit-save').addEventListener('click', saveEditLabel);
        document.getElementById('edit-cancel').addEventListener('click', function() { editLabelModal.classList.add('hidden'); state.editingLabelId = null; });
        document.getElementById('edit-close').addEventListener('click', function() { editLabelModal.classList.add('hidden'); state.editingLabelId = null; });
        variableSearch.addEventListener('input', function(e) { renderVariableTree(getEffectiveVariableTree(), '', variableTree, e.target.value); });
        downloadBtn.addEventListener('click', function() { downloadDocument(); });
        applyHighlightsBtn.addEventListener('click', function() { downloadDocument({ forceHighlights: true }); });
        document.getElementById('settings-btn').addEventListener('click', function() {
            document.getElementById('additional-instructions').value = state.settings.additionalInstructions;
            document.getElementById('context-text').value = state.settings.contextText || '';
            document.getElementById('custom-people').value = state.settings.customPeople;
            promptProfileInput.value = state.settings.promptProfile || 'standard';
            generationMethodInput.value = state.settings.generationMethod || 'multi_run';
            generatorModelsInput.value = state.settings.generatorModels || '';
            judgeModelInput.value = state.settings.judgeModel || '';
            document.getElementById('defragment-runs').checked = !!state.settings.defragmentRuns;
            usePlaygroundVariablesInput.checked = !!state.settings.usePlaygroundVariables;
            aiModelInput.value = state.settings.model;
            state.ui.editInterviewSourceInSettings = false;
            renderGenerationMethodFields();
            renderInterviewPicker();
            renderModelSuggestions('');
            settingsModal.classList.remove('hidden');
        });
        document.getElementById('close-settings').addEventListener('click', function() {
            state.ui.editInterviewSourceInSettings = false;
            renderInterviewPicker();
            settingsModal.classList.add('hidden');
        });
        document.getElementById('save-settings').addEventListener('click', function() {
            state.settings.additionalInstructions = document.getElementById('additional-instructions').value;
            state.settings.contextText = document.getElementById('context-text').value;
            state.settings.customPeople = document.getElementById('custom-people').value;
            state.settings.promptProfile = promptProfileInput.value || 'standard';
            state.settings.generationMethod = generationMethodInput.value || 'multi_run';
            state.settings.generatorModels = generatorModelsInput.value;
            state.settings.judgeModel = judgeModelInput.value.trim();
            state.settings.defragmentRuns = !!document.getElementById('defragment-runs').checked;
            state.settings.usePlaygroundVariables = !!usePlaygroundVariablesInput.checked;
            state.settings.model = aiModelInput.value.trim() || state.defaultModel;
            state.ui.editInterviewSourceInSettings = false;
            renderInterviewPicker();
            settingsModal.classList.add('hidden');
        });
        document.getElementById('reset-settings').addEventListener('click', function() {
            document.getElementById('additional-instructions').value = '';
            document.getElementById('context-text').value = '';
            document.getElementById('custom-people').value = '';
            promptProfileInput.value = 'standard';
            generationMethodInput.value = 'multi_run';
            generatorModelsInput.value = '';
            judgeModelInput.value = '';
            document.getElementById('defragment-runs').checked = true;
            usePlaygroundVariablesInput.checked = false;
            aiModelInput.value = state.defaultModel;
            renderGenerationMethodFields();
            renderInterviewPicker();
            renderModelSuggestions('');
        });
        if (changeInterviewSourceBtn) {
            changeInterviewSourceBtn.addEventListener('click', function() {
                state.ui.editInterviewSourceInSettings = !state.ui.editInterviewSourceInSettings;
                renderInterviewPicker();
            });
        }
        generationMethodInput.addEventListener('change', renderGenerationMethodFields);
        aiModelInput.addEventListener('input', function() { renderModelSuggestions(aiModelInput.value); });
        aiModelInput.addEventListener('change', function() {
            state.settings.model = aiModelInput.value.trim() || state.defaultModel;
        });
        interviewSourceModeInputs.forEach(function(input) {
            input.addEventListener('change', async function() {
                if (!input.checked) return;
                state.interviewSourceMode = input.value === 'installed' ? 'installed' : 'playground';
                renderInterviewPicker();
                if (state.interviewSourceMode === 'installed') {
                    if (!state.installed.packages.length) {
                        await fetchInstalledPackages();
                    } else if (state.installed.selectedFile) {
                        await fetchInstalledVariables();
                    }
                } else {
                    if (!state.playground.projects.length) {
                        await fetchPlaygroundProjects();
                    } else if (state.playground.selectedFile) {
                        await fetchPlaygroundVariables();
                    }
                }
            });
        });
        playgroundProjectSelect.addEventListener('change', async function() {
            state.playground.selectedProject = playgroundProjectSelect.value || 'default';
            state.playground.selectedFile = '';
            await fetchPlaygroundFiles();
        });
        playgroundYamlFileSelect.addEventListener('change', async function() {
            state.playground.selectedFile = playgroundYamlFileSelect.value || '';
            await fetchPlaygroundVariables();
        });
        installedPackageSelect.addEventListener('change', async function() {
            state.installed.selectedPackage = installedPackageSelect.value || '';
            state.installed.selectedFile = '';
            await fetchInstalledFiles();
        });
        installedYamlFileSelect.addEventListener('change', async function() {
            state.installed.selectedFile = installedYamlFileSelect.value || '';
            await fetchInstalledVariables();
        });
        usePlaygroundVariablesInput.addEventListener('change', function() {
            state.settings.usePlaygroundVariables = !!usePlaygroundVariablesInput.checked;
        });
        toggleLowConfidence.checked = !!state.settings.showLowConfidence;
        toggleLowConfidence.addEventListener('change', function() {
            state.settings.showLowConfidence = !!toggleLowConfidence.checked;
            renderSuggestions();
        });
        [settingsModal, bulkReplaceModal, editLabelModal, utilitiesModal, repairModal].forEach(function(modal) {
            modal.addEventListener('click', function(e) {
                if (e.target === modal) {
                    if (modal === settingsModal) {
                        state.ui.editInterviewSourceInSettings = false;
                        renderInterviewPicker();
                    }
                    modal.classList.add('hidden');
                }
            });
        });

        // Selection popover events
        previewContent.addEventListener('mouseup', onPreviewMouseUp);
        document.getElementById('sel-close').addEventListener('click', hideSelectionPopover);
        document.getElementById('sel-cancel').addEventListener('click', hideSelectionPopover);
        document.getElementById('sel-save').addEventListener('click', saveSelectionLabel);
        selVarInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); saveSelectionLabel(); } });
        document.getElementById('sel-tree-toggle').addEventListener('click', function() {
            if (selVarPanel.classList.contains('hidden')) {
                selVarPanel.classList.remove('hidden');
                renderSelPopoverTree();
            } else {
                selVarPanel.classList.add('hidden');
            }
        });
        selVarSearch.addEventListener('input', function(e) { renderSelPopoverTree(e.target.value); });
        // Close popover on outside click
        document.addEventListener('mousedown', function(e) {
            if (!selPopover.classList.contains('hidden') && !selPopover.contains(e.target) && !previewContent.contains(e.target)) {
                hideSelectionPopover();
            }
            var authMenu = document.getElementById('auth-menu');
            var authMenuBtn = document.getElementById('auth-menu-btn');
            if (authMenu && authMenuBtn && !authControls.contains(e.target)) {
                authMenu.classList.remove('show');
            }
        });

        // Drag & drop
        var dropZone = document.querySelector('aside');
        dropZone.addEventListener('dragover', function(e) { e.preventDefault(); dropZone.classList.add('drag-over'); });
        dropZone.addEventListener('dragleave', function() { dropZone.classList.remove('drag-over'); });
        dropZone.addEventListener('drop', function(e) {
            e.preventDefault();
            dropZone.classList.remove('drag-over');
            var file = e.dataTransfer.files[0];
            if (file && file.name.endsWith('.docx')) processFile(file);
        });

        fetchModelCatalog();
        fetchAuthStatus();
        renderGenerationMethodFields();
        updateModalSourceStatus();
    })();
    </script>
</body>
</html>
