GPT-Navigation

ChatGPT 对话导航、问题答案导出与搜索定位增强脚本。

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name               GPT-Navigation
// @version            1.0.0
// @license            GPL-3.0-or-later
// @author             凌致
// @description        ChatGPT 对话导航、问题答案导出与搜索定位增强脚本。
// @match              https://chat.openai.com/**
// @match              https://chatgpt.com/**
// @icon               https://t1.gstatic.cn/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&size=32&url=https://chatgpt.com
// @namespace          ai-conversation-navigator
// @run-at             document-idle
// @grant              none
// ==/UserScript==

(function() {
    'use strict';

    // ============================================================
    // ==Config== 配置层 - 常量、选择器、开关
    // ============================================================

    // [START: 配置常量]
    const DOM_MARK = 'data-ai-conversation-navigator';
    const STORAGE_PREFIX = 'ai_navigator';
    const CHATGPT_MESSAGE_SELECTOR = '[data-message-author-role]';
    // Gemini selectors removed (GPT-only build)

    let conversationKey = null;
    let loaded = false;
    let activeIndex = null;
    let refreshScheduled = false;
    // [END: 配置常量]

    const cssText = `
        :host {
            --brand-primary: #7fbf7b;
            --brand-primary-strong: #5f9f5c;
            --panel-bg: rgba(240, 248, 237, 0.98);
            --panel-border: rgba(95, 159, 92, 0.18);
            --panel-shadow: 0 18px 45px rgba(80, 112, 78, 0.18);
            --text-primary: #203126;
            --text-secondary: #5f7461;
            --surface-muted: rgba(230, 241, 227, 0.96);
            --line-strong: rgba(22, 33, 25, 0.16);
            --panel-width: 336px;
            --panel-max-height: min(72vh, 760px);
            display: block;
            position: fixed;
            top: 10vh;
            right: 16px;
            z-index: 2147483647;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            color: var(--text-primary);
            font-size: 13px;
            user-select: none;
        }
        .app-shell {
            position: relative;
        }
        .panel {
            position: relative;
            right: 0;
            width: var(--panel-width);
            max-height: var(--panel-max-height);
            display: flex;
            flex-direction: column;
            border-radius: 22px;
            background: linear-gradient(180deg, color-mix(in srgb, var(--panel-bg) 98%, white 2%), color-mix(in srgb, var(--panel-bg) 92%, transparent 8%));
            border: 1px solid var(--panel-border);
            box-shadow: 0 24px 64px rgba(15, 23, 42, 0.14), 0 2px 0 rgba(255, 255, 255, 0.35) inset;
            backdrop-filter: blur(22px) saturate(1.08);
            overflow: hidden;
        }
        .header {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 12px 12px 10px;
            border-bottom: 1px solid var(--line-strong);
            cursor: move;
            background: linear-gradient(180deg, color-mix(in srgb, var(--panel-bg) 94%, white 6%), color-mix(in srgb, var(--panel-bg) 74%, transparent 26%));
        }
        .title-block {
            flex: 1;
            min-width: 0;
        }
        .header-actions {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            padding: 3px;
            border-radius: 14px;
            background: color-mix(in srgb, var(--panel-bg) 80%, transparent 20%);
            border: 1px solid var(--line-strong);
            box-shadow: inset 0 1px 0 rgba(255,255,255,0.75);
        }
        .title {
            font-size: 14px;
            font-weight: 700;
            line-height: 1.15;
            letter-spacing: -0.01em;
        }
        .subtitle {
            margin-top: 2px;
            color: var(--text-secondary);
            font-size: 10px;
        }

        .icon-btn {
            width: 30px;
            height: 30px;
            border: 1px solid rgba(15, 23, 42, 0.07);
            border-radius: 10px;
            background: linear-gradient(180deg, rgba(255,255,255,0.96), rgba(248,250,252,0.92));
            color: var(--text-primary);
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            font-size: 13px;
            flex-shrink: 0;
            box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
            transition: transform 0.14s ease, border-color 0.14s ease, background 0.14s ease, color 0.14s ease, box-shadow 0.14s ease;
        }
        .icon-btn[data-active="true"] {
            color: var(--brand-primary-strong);
            border-color: rgba(25, 195, 125, 0.28);
            background: linear-gradient(180deg, rgba(220,252,231,0.98), rgba(209,250,229,0.9));
            box-shadow: 0 8px 18px rgba(25, 195, 125, 0.12);
        }
        .icon-btn:hover {
            border-color: rgba(25, 195, 125, 0.35);
            color: var(--brand-primary-strong);
            background: #fff;
            box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
            transform: translateY(-1px);
        }
        .content {
            display: flex;
            flex-direction: column;
            min-height: 0;
        }
        :host(:not([open])) .panel {
            width: auto;
            max-height: none;
            overflow: visible;
            border-radius: 18px;
        }
        :host(:not([open])) .header {
            padding: 8px;
            border-bottom: none;
            background: transparent;
            cursor: default;
        }
        :host(:not([open])) .title-block,
        :host(:not([open])) .content,
        :host(:not([open])) .header-actions > :not(.toggle-eye) {
            display: none !important;
        }
        :host(:not([open])) .header-actions {
            padding: 0;
            border: none;
            background: transparent;
            box-shadow: none;
        }
        .list-container {
            position: relative;
            padding: 8px;
            max-height: 44vh;
            overflow-y: auto;
            scrollbar-width: thin;
        }
        .list-container::-webkit-scrollbar {
            width: 6px;
        }
        .list-container::-webkit-scrollbar-thumb {
            background: rgba(107, 114, 128, 0.35);
            border-radius: 999px;
        }
        ul {
            list-style: none;
            margin: 0;
            padding: 0;
        }
        li {
            padding: 10px 11px;
            margin-bottom: 5px;
            border-radius: 12px;
            cursor: pointer;
            color: var(--text-secondary);
            background: linear-gradient(180deg, rgba(248,250,252,0.95), rgba(244,247,250,0.88));
            border: 1px solid rgba(15, 23, 42, 0.04);
            line-height: 1.35;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
        }
        .question-item {
            padding: 0;
            overflow: hidden;
        }
        .question-row {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 10px 11px;
            position: sticky;
            top: 0;
            z-index: 1;
            background: inherit;
        }
        .question-text {
            flex: 1;
            min-width: 0;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .expand-btn {
            width: 24px;
            height: 24px;
            border: 1px solid rgba(15, 23, 42, 0.08);
            border-radius: 8px;
            background: rgba(255,255,255,0.88);
            color: var(--text-secondary);
            cursor: pointer;
            flex-shrink: 0;
        }
        .expand-btn:hover {
            color: var(--brand-primary-strong);
            border-color: rgba(25, 195, 125, 0.24);
        }
        .question-item.active .question-row {
            color: var(--brand-primary-strong);
            font-weight: 600;
        }
        .answer-tree {
            display: flex;
            flex-direction: column;
            gap: 4px;
            padding: 0 8px 8px 8px;
        }
        .answer-node {
            padding: 7px 10px;
            border-radius: 10px;
            color: var(--text-secondary);
            background: rgba(255,255,255,0.7);
            border: 1px solid rgba(15, 23, 42, 0.04);
            font-size: 12px;
            cursor: pointer;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .answer-node:hover {
            color: var(--brand-primary-strong);
            border-color: rgba(25, 195, 125, 0.14);
        }
        li:hover {
            color: var(--brand-primary-strong);
            background: rgba(220, 252, 231, 0.9);
            transform: translateX(-1px);
            border-color: rgba(25, 195, 125, 0.14);
        }
        li.active {
            color: var(--brand-primary-strong);
            background: linear-gradient(180deg, rgba(220,252,231,0.98), rgba(209,250,229,0.94));
            font-weight: 600;
            border-color: rgba(25, 195, 125, 0.2);
            box-shadow: inset 0 0 0 1px rgba(25, 195, 125, 0.08), 0 8px 16px rgba(25, 195, 125, 0.08);
        }
        .fade-bottom {
            position: sticky;
            bottom: 0;
            width: 100%;
            height: 16px;
            margin-top: -16px;
            background: linear-gradient(to top, rgba(255,255,255,0.94), rgba(255,255,255,0));
            pointer-events: none;
        }

        .text-input,
            display: flex;
            flex-direction: column;
            gap: 6px;
        }
        .field-row {
            display: grid;
            grid-template-columns: repeat(2, minmax(0, 1fr));
            gap: 8px;
        }
        .field-label {
            color: var(--text-primary);
            font-size: 12px;
            font-weight: 600;
        }
        .range-row,
        .toggle-row,
        .style-row {
            display: flex;
            gap: 6px;
            align-items: center;
            width: 100%;
        }
        .range-row.column {
            flex-direction: column;
            align-items: stretch;
            gap: 10px;
        }
        .range-row span {
            color: var(--text-secondary);
            font-size: 12px;
        }
        .text-input {
            width: 100%;
            box-sizing: border-box;
            border: 1px solid rgba(15, 23, 42, 0.1);
            border-radius: 12px;
            background: color-mix(in srgb, var(--panel-bg) 88%, white 12%);
            color: var(--text-primary);
            box-shadow: inset 0 1px 1px rgba(15, 23, 42, 0.02);
            font: inherit;
        }
        .text-input {
            min-height: 42px;
            padding: 10px 12px;
        }
        .text-area {
            min-height: 132px;
            padding: 12px;
            resize: vertical;
            line-height: 1.5;
        }
        .primary-btn {
            min-height: 40px;
            padding: 0 14px;
            border: 1px solid #6eaf6a;
            border-radius: 12px;
            background: linear-gradient(180deg, #8dc887, #72b46e);
            color: #fff;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            box-shadow: 0 8px 18px rgba(95, 159, 92, 0.18);
        }
        .primary-btn:hover {
            filter: brightness(1.02);
        }
        .select-check {
            appearance: none;
            -webkit-appearance: none;
            width: 18px;
            height: 18px;
            margin: 0;
            border: 1.5px solid #000;
            border-radius: 5px;
            background: color-mix(in srgb, var(--panel-bg) 92%, white 8%);
            cursor: pointer;
            flex-shrink: 0;
            position: relative;
            box-shadow: inset 0 1px 1px rgba(255,255,255,0.6);
        }
        .select-check:checked {
            background: linear-gradient(180deg, #9fd49a, #7fbf7b);
            border-color: #000;
        }
        .select-check:checked::after {
            content: '';
            position: absolute;
            left: 5px;
            top: 1px;
            width: 4px;
            height: 9px;
            border: solid #14311c;
            border-width: 0 2px 2px 0;
            transform: rotate(45deg);
        }
        .mini-btn {
            min-width: 46px;
            height: 28px;
            padding: 0 10px;
            border: 1px solid rgba(15, 23, 42, 0.08);
            border-radius: 9px;
            background: rgba(255,255,255,0.96);
            color: var(--text-primary);
            font-size: 12px;
            cursor: pointer;
        }
        .mini-btn:hover {
            border-color: rgba(25, 195, 125, 0.28);
            color: var(--brand-primary-strong);
        }
        .mini-btn.danger:hover {
            border-color: rgba(239, 68, 68, 0.26);
            color: #dc2626;
        }
        .ghost-btn {
            min-height: 40px;
            padding: 0 14px;
            border: 1px solid rgba(15, 23, 42, 0.1);
            border-radius: 12px;
            background: rgba(255,255,255,0.96);
            color: var(--text-primary);
            font-size: 13px;
            font-weight: 600;
            cursor: pointer;
        }
        .ghost-btn:hover {
            border-color: rgba(25, 195, 125, 0.24);
            color: var(--brand-primary-strong);
        }
        .button-row {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }
        .button-row > * {
            flex: 1;
        }
        .tag-chip {
            display: inline-flex;
            align-items: center;
            width: fit-content;
            margin-top: 6px;
            padding: 3px 8px;
            border-radius: 999px;
            background: rgba(226, 232, 240, 0.75);
            color: var(--text-secondary);
            font-size: 11px;
            line-height: 1;
        }
        .status-line {
            margin-top: 6px;
            color: var(--text-secondary);
            font-size: 12px;
            line-height: 1.5;
        }
        .status-line[data-tone="success"] {
            color: #047857;
        }
        .status-line[data-tone="warning"] {
            color: #b45309;
        }
        .status-line[data-tone="danger"] {
            color: #b91c1c;
        }
        .editor-meta {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 8px;
            margin-top: 10px;
        }
        .editor-badge {
            display: inline-flex;
            align-items: center;
            min-height: 28px;
            padding: 0 10px;
            border-radius: 999px;
            background: rgba(226, 232, 240, 0.8);
            color: var(--text-secondary);
            font-size: 12px;
            font-weight: 600;
        }
        .toolbar-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 10px;
            margin-bottom: 8px;
        }
        .toolbar-row.stack {
            align-items: stretch;
            flex-direction: column;
            gap: 10px;
        }
        .toolbar-summary {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 8px;
        }
        .toolbar-head {
            display: flex;
            justify-content: flex-start;
            gap: 8px;
            align-items: center;
            flex-wrap: wrap;
        }
        .toolbar-actions {
            display: flex;
            align-items: center;
            gap: 6px;
            flex-wrap: wrap;
            justify-content: flex-end;
        }
        .master-check {
            display: inline-grid;
            grid-auto-flow: column;
            grid-template-columns: 18px auto;
            gap: 8px;
            align-items: center;
            justify-content: center;
            height: 38px;
            padding: 0 10px;
            border: 1px solid var(--line-strong);
            border-radius: 12px;
            background: color-mix(in srgb, var(--panel-bg) 88%, var(--surface-muted) 12%);
            color: var(--text-primary);
            font-size: 12px;
            font-weight: 700;
            box-sizing: border-box;
            vertical-align: middle;
        }
        .master-check .select-check {
            display: block;
            align-self: center;
            justify-self: center;
        }
        .master-check span {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            height: 100%;
            line-height: 1;
            white-space: nowrap;
        }
        .helper-text {
            margin-top: 6px;
            color: var(--text-secondary);
            font-size: 12px;
            line-height: 1.5;
        }
        .field-select {
            width: 100%;
            min-height: 42px;
            padding: 0 12px;
            border-radius: 12px;
            border: 1px solid var(--line-strong);
            background: color-mix(in srgb, var(--panel-bg) 86%, white 14%);
            color: var(--text-primary);
            font: inherit;
        }
        .field-row-2 {
            display: grid;
            grid-template-columns: repeat(2, minmax(0, 1fr));
            gap: 10px;
        }
        .inline-action-row {
            display: grid;
            grid-template-columns: minmax(0, 1fr) auto;
            gap: 8px;
            align-items: end;
        }
        .menu-wrap {
            position: relative;
        }
        .menu-panel {
            position: absolute;
            top: calc(100% + 8px);
            right: 0;
            width: 204px;
            padding: 8px;
            border-radius: 14px;
            border: 1px solid var(--line-strong);
            background: color-mix(in srgb, var(--panel-bg) 96%, transparent 4%);
            box-shadow: var(--panel-shadow);
            display: none;
            z-index: 20;
        }
        .menu-panel.show {
            display: block;
        }
        .menu-panel.disabled {
            display: none !important;
        }
        .menu-item {
            width: 100%;
            min-height: 36px;
            border: 1px solid transparent;
            border-radius: 10px;
            background: transparent;
            color: var(--text-primary);
            text-align: left;
            cursor: pointer;
            padding: 0 10px;
        }
        .menu-item:hover {
            background: var(--surface-muted);
            border-color: var(--line-strong);
        }
        .search-bar {
            display: grid;
            grid-template-columns: minmax(0, 1fr) auto auto;
            gap: 8px;
            margin-bottom: 0;
            align-items: stretch;
        }
        .search-bar .text-input {
            min-height: 30px;
            padding: 4px 12px;
        }

        .icon-mini-btn {
            width: 40px;
            height: 38px;
            border: 1px solid rgba(15, 23, 42, 0.1);
            border-radius: 12px;
            background: rgba(255,255,255,0.96);
            color: var(--text-primary);
            cursor: pointer;
            font-size: 15px;
        }
        .icon-mini-btn:hover {
            border-color: rgba(25, 195, 125, 0.24);
            color: var(--brand-primary-strong);
        }
        .search-hit {
            display: inline;
            padding: 0 1px;
            border-radius: 3px;
            background: var(--search-hit-bg, #fff59d) !important;
            color: var(--search-hit-text, #111827) !important;
            box-shadow: inset 0 0 0 1px var(--search-hit-outline, rgba(161, 98, 7, 0.18));
            box-decoration-break: clone;
            -webkit-box-decoration-break: clone;
        }
        .current-search-hit {
            background: var(--search-hit-current-bg, #f7c948) !important;
            color: var(--search-hit-current-text, #111827) !important;
            box-shadow: 0 0 0 2px var(--search-hit-shadow, rgba(245, 158, 11, 0.28));
        }
        .search-row {
            display: grid;
            grid-template-columns: minmax(0, 1fr) auto auto;
            gap: 8px;
            align-items: end;
        }
        .selection-row {
            display: flex;
            align-items: center;
            gap: 8px;
            flex-wrap: wrap;
        }
        .selection-row > * {
            margin: 0;
            align-self: center;
        }
        .selection-row .editor-badge,
        .selection-row .compact-btn,
        .selection-row .master-check {
            min-width: 108px;
            min-height: 38px;
            box-sizing: border-box;
        }
        .selection-row .editor-badge {
            margin-left: 0;
        }
        .selection-row .master-check {
            display: inline-flex;
            align-items: center;
            justify-content: flex-start;
            padding-left: 12px;
            padding-right: 12px;
            padding-top: 0;
            padding-bottom: 0;
        }
        .selection-row .master-check .select-check {
            position: relative;
            top: 2px;
            flex-shrink: 0;
        }
        .selection-row .master-check.checkbox-only {
            min-width: 38px;
            width: 38px;
            padding-left: 0;
            padding-right: 0;
            justify-content: center;
            gap: 0;
        }
        .selection-row .master-check.checkbox-only span {
            display: none;
        }
        .selection-row .master-check.checkbox-only .select-check {
            top: 1px;
        }
        .selection-row .master-check span {
            display: inline-flex;
            align-items: center;
            min-height: 0;
            height: auto;
        }
        .compact-btn {
            min-width: 108px;
            min-height: 38px;
            padding: 0 12px;
            border: 1px solid var(--line-strong);
            border-radius: 12px;
            background: color-mix(in srgb, var(--panel-bg) 88%, var(--surface-muted) 12%);
            color: var(--text-primary);
            font-size: 12px;
            font-weight: 700;
            cursor: pointer;
        }
        .compact-btn.danger-btn:hover {
            border-color: rgba(239, 68, 68, 0.26);
            color: #dc2626;
        }
        .compact-btn:hover {
            border-color: rgba(25, 195, 125, 0.28);
            color: var(--brand-primary-strong);
        }
        .setting-stack {
            display: flex;
            flex-direction: column;
            gap: 12px;
            width: 100%;
        }
        .setting-row-2 {
            display: grid;
            grid-template-columns: repeat(2, minmax(0, 1fr));
            gap: 12px;
            width: 100%;
        }
        .setting-inline {
            display: grid;
            grid-template-columns: minmax(0, 1fr) auto;
            gap: 8px;
            align-items: center;
        }
        .setting-inline.equal {
            grid-template-columns: repeat(2, minmax(0, 1fr));
        }
        .setting-inline .number-input,
        .setting-inline .field-select,
        .setting-inline .ghost-btn,
        .setting-inline .primary-btn {
            width: 100%;
        }
        .setting-inline .primary-btn {
            min-width: 132px;
        }
        .setting-field {
            display: flex;
            flex-direction: column;
            gap: 6px;
        }
        .drop-zone {
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 140px;
            padding: 16px;
            border-radius: 14px;
            border: 1px dashed rgba(15, 23, 42, 0.16);
            background: rgba(255,255,255,0.7);
            color: var(--text-secondary);
            text-align: center;
            line-height: 1.6;
        }
        .drop-zone.dragover {
            border-color: rgba(25, 195, 125, 0.4);
            background: rgba(240, 253, 244, 0.85);
            color: var(--brand-primary-strong);
        }
        .sr-file {
            display: none;
        }
        .choice-row {
            display: flex;
            gap: 6px;
            flex-wrap: wrap;
        }
        .choice-row.grid-2 {
            display: grid;
            grid-template-columns: repeat(2, minmax(0, 1fr));
        }
        .choice-btn,
        .style-dot,
        .type-btn {
            border: 1px solid rgba(15, 23, 42, 0.1);
            background: #fff;
            color: var(--text-primary);
            border-radius: 12px;
            cursor: pointer;
        }
        .choice-btn {
            padding: 6px 10px;
            font-size: 11px;
            min-height: 34px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            gap: 6px;
            box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
            transition: transform 0.14s ease, border-color 0.14s ease, box-shadow 0.14s ease, background 0.14s ease, color 0.14s ease;
        }
        .choice-btn span:last-child {
            font-weight: 600;
            letter-spacing: 0.01em;
        }
        .choice-btn[data-value="default"] {
            background: linear-gradient(180deg, #ffffff, #f3f4f6);
            color: #111827;
        }
        .choice-btn.active,
        .type-btn.active {
            background: linear-gradient(180deg, #8dc887, #72b46e);
            color: #fff;
            border-color: #6eaf6a;
        }
        .choice-btn:hover,
        .style-dot:hover,
        .type-btn:hover {
            border-color: rgba(25, 195, 125, 0.45);
            transform: translateY(-1px);
        }
        .choice-btn.active {
            box-shadow: 0 8px 18px rgba(25, 195, 125, 0.2);
        }
        .toggle-row {
            justify-content: space-between;
        }
        .style-row {
            justify-content: space-between;
        }
        .style-dots {
            display: flex;
            gap: 8px;
        }
        .style-dot {
            width: 26px;
            height: 26px;
            border-radius: 999px;
        }
        .style-dot.active {
            outline: 3px solid rgba(25, 195, 125, 0.2);
            border-color: var(--brand-primary);
        }
        .type-switch {
            display: flex;
            gap: 8px;
            justify-content: flex-end;
        }
        .type-btn {
            padding: 6px 10px;
            font-size: 11px;
        }
        .range-row .number-input,
        .range-row button {
            height: 40px;
            border: 1px solid rgba(15, 23, 42, 0.1);
            border-radius: 10px;
            background: rgba(255,255,255,0.96);
            color: var(--text-primary);
        }
        .range-row button {
            width: 100%;
            padding: 0 14px;
            background: linear-gradient(180deg, #8dc887, #72b46e);
            color: #fff;
            border-color: #6eaf6a;
            white-space: nowrap;
            cursor: pointer;
            box-shadow: 0 8px 18px rgba(95, 159, 92, 0.18);
            font-size: 14px;
            font-weight: 600;
        }
        .inline-field {
            display: flex;
            align-items: center;
            gap: 6px;
        }
        .inline-field span {
            color: var(--text-secondary);
            font-size: 12px;
            white-space: nowrap;
        }
        .switch-pair {
            display: grid;
            grid-template-columns: repeat(2, minmax(0, 1fr));
            gap: 8px;
        }
        .switch-item {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 8px;
            min-height: 34px;
            padding: 0 1px;
        }
        .theme-chip {
            width: 12px;
            height: 12px;
            border-radius: 999px;
            border: 1px solid rgba(15, 23, 42, 0.1);
            flex-shrink: 0;
            box-shadow: inset 0 1px 1px rgba(255,255,255,0.55);
        }
        @media (max-width: 640px) {
            :host {
                top: 12px;
                right: 12px;
            }
            .panel {
                width: min(320px, calc(100vw - 24px));
            }
            .field-row,
            .choice-row.grid-2,
            .switch-pair,
            .search-row,
            .question-row {
                padding-right: 8px;
            }
        }
    `;
    // [END: CSS样式]

    // ============================================================
    // ==State== 状态层 - 全局状态管理
    // ============================================================

    // [START: 全局状态]
    // 注意: 使用原有的全局变量作为状态管理
    // conversationKey, loaded, activeIndex, refreshScheduled, geminiBootstrapStarted
    // [END: 全局状态]

    // ============================================================
    // ==Utils== 工具层 - 纯函数工具
    // ============================================================

    // [START: 工具函数]

    function detectSite() {
        return 'chatgpt';
    }
    async function copyText(text) {
        if (navigator.clipboard?.writeText) {
            await navigator.clipboard.writeText(text);
            return true;
        }
        const area = document.createElement('textarea');
        area.value = text;
        area.setAttribute('readonly', 'readonly');
        area.style.position = 'fixed';
        area.style.opacity = '0';
        document.body.appendChild(area);
        area.select();
        area.setSelectionRange(0, area.value.length);
        const success = document.execCommand('copy');
        area.remove();
        if (!success) throw new Error('copy failed');
        return true;
    }

    function scrollToNode(target, block = 'start') {
        if (!target) return;
        if (!isChatGPTSharePage()) target.style.scrollMarginTop = '56px';
        try {
            target.scrollIntoView({ behavior: 'smooth', block });
        } catch {
            target.scrollIntoView();
        }
    }

    function isChatGPTSharePage() {
        return location.pathname.startsWith('/share/');
    }

    function getPositionStorageKey() {
        return `${STORAGE_PREFIX}_position_${detectSite()}`;
    }

    function sanitizeText(text) {
        return (text || '').replace(/\s+/g, ' ').trim();
    }

    function getRootNodes(root = document) {
        const roots = [root];
        const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
        let current = walker.currentNode;
        while (current) {
            if (current.shadowRoot) {
                roots.push(current.shadowRoot);
            }
            current = walker.nextNode();
        }
        return roots;
    }

    function queryAllDeep(selector, root = document) {
        const results = [];
        getRootNodes(root).forEach(searchRoot => {
            results.push(...Array.from(searchRoot.querySelectorAll(selector)));
        });
        return Array.from(new Set(results));
    }

    function getClosestMessageWrapper(element) {
        if (!element) return null;
        return element.closest('[data-testid^="conversation-turn-"]')
            || element.closest('[data-testid]')
            || element.closest('article')
            || element.closest('section')
            || element.parentElement;
    }

    function getSiteTitle() {
        return 'ChatGPT';
    }

    function getConversationKey() {
        return `chatgpt::${location.pathname}::${location.search}`;
    }

    function queryChatGPTContainer() {
        return document.querySelector('main')?.querySelector('.flex.flex-col.text-sm') || null;
    }

    async function ensureFullDOMRendered() {
        const scrollContainer = queryScrollContainer();
        if (!scrollContainer) return;
        const originalOverflow = scrollContainer.style.overflow;
        scrollContainer.style.overflow = 'hidden';
        const originalScrollTop = scrollContainer.scrollTop;
        const maxScroll = scrollContainer.scrollHeight;
        const step = Math.min(800, scrollContainer.clientHeight * 2);
        let pos = 0;
        while (pos < maxScroll) {
            scrollContainer.scrollTop = pos;
            pos += step;
            await new Promise(r => setTimeout(r, 50));
        }
        scrollContainer.scrollTop = originalScrollTop;
        scrollContainer.style.overflow = originalOverflow;
    }

    function queryChatGPTQuestionElements() {
        const directUsers = queryAllDeep('[data-message-author-role="user"]');
        const wrapped = directUsers
            .map(getClosestMessageWrapper)
            .filter(Boolean);
        return Array.from(new Set(wrapped)).sort((a, b) => {
            if (a === b) return 0;
            const pos = a.compareDocumentPosition(b);
            if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
            if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
            return 0;
        });
    }

    function queryQuestionElements() {
        return queryChatGPTQuestionElements();
    }

    function queryScrollContainer() {
        return queryChatGPTContainer()?.parentElement || document.scrollingElement || document.documentElement;
    }

    // [END: 工具函数]

    // ============================================================
    // ==Logic== 业务层 - 功能模块
    // ============================================================

    // [START: NavigatorApp 组件]
    class NavigatorApp extends HTMLElement {
        // AI对话导航器主组件 - 包含UI渲染、事件处理、数据管理等功能
        constructor() {
            super();
            this.attachShadow({ mode: 'open' });
            this._isOpen = false;

            this._suppressEyeClick = false;
            this._allQuestions = [];
            this._allElements = [];
            this._allDetails = [];
            this._allQuestionIds = [];
            this._questions = [];
            this._elements = [];
            this._details = [];
            this._questionIds = [];
            this._expandedQuestions = new Set();
            this._selectedQuestionIds = new Set();
            this._searchKeyword = '';
            this._searchResults = [];
            this._searchIndex = -1;
        }

        connectedCallback() {
            this.render();
            this.loadPosition();
            this.updateOpenState();
            this.startTracking();
            this.initDraggable();
        }

        loadPosition() {
            try {
                const pos = JSON.parse(localStorage.getItem(getPositionStorageKey()) || '{}');
                if (pos.top) this.style.top = pos.top;
                if (pos.right) this.style.right = pos.right;
                if (pos.left) this.style.left = pos.left;
            } catch {}
        }

        savePosition() {
            localStorage.setItem(getPositionStorageKey(), JSON.stringify({
                top: this.style.top,
                right: this.style.right,
                left: this.style.left
            }));
        }

        anchorToRight(rect = this.getBoundingClientRect()) {
            const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0;
            const anchoredRight = Math.max(0, viewportWidth - rect.right);
            this.style.left = 'auto';
            this.style.right = `${anchoredRight}px`;
            this.style.top = `${rect.top}px`;
        }

        initDraggable() {
            const header = this.shadowRoot.querySelector('.header');
            if (!header) return;

            let startX = 0;
            let startY = 0;
            let startRight = 0;
            let startTop = 0;
            let startWidth = 0;
            let dragging = false;

            const onMove = (event) => {
                dragging = true;
                const dx = event.clientX - startX;
                const dy = event.clientY - startY;
                const nextLeft = (window.innerWidth || document.documentElement.clientWidth || 0) - startRight - startWidth + dx;
                const nextRight = Math.max(0, (window.innerWidth || document.documentElement.clientWidth || 0) - (nextLeft + startWidth));
                this.style.left = 'auto';
                this.style.right = `${nextRight}px`;
                this.style.top = `${startTop + dy}px`;
            };

            const onUp = () => {
                document.removeEventListener('mousemove', onMove);
                document.removeEventListener('mouseup', onUp);
                if (dragging) {
                    this._suppressEyeClick = true;
                    this.savePosition();
                    setTimeout(() => {
                        this._suppressEyeClick = false;
                    }, 0);
                }
            };

            header.addEventListener('mousedown', (event) => {
                const eyeButton = event.target.closest('.toggle-eye');
                if (event.target.closest('.icon-btn') && !eyeButton) return;
                if (!this._isOpen && !eyeButton) return;
                dragging = false;
                startX = event.clientX;
                startY = event.clientY;
                const rect = this.getBoundingClientRect();
                startRight = Math.max(0, (window.innerWidth || document.documentElement.clientWidth || 0) - rect.right);
                startTop = rect.top;
                startWidth = rect.width;
                this.style.left = 'auto';
                this.style.right = `${startRight}px`;
                this.style.top = `${rect.top}px`;
                document.addEventListener('mousemove', onMove);
                document.addEventListener('mouseup', onUp);
            });
        }
        getVisibleQuestionIds() {
            return this._questionIds.filter(Boolean);
        }

        toggleQuestionSelection(id, forceState = null) {
            if (!id) return;
            const shouldSelect = forceState === null ? !this._selectedQuestionIds.has(id) : !!forceState;
            if (shouldSelect) this._selectedQuestionIds.add(id);
            else this._selectedQuestionIds.delete(id);
            this.updateList();
        }

        setVisibleQuestionSelection(checked) {
            this.getVisibleQuestionIds().forEach((id) => {
                if (checked) this._selectedQuestionIds.add(id);
                else this._selectedQuestionIds.delete(id);
            });
            this.updateList();
        }

        clearQuestionSelection() {
            this._selectedQuestionIds.clear();
            this.updateList();
        }

        clearSearchHighlights() {
            const normalizeTargets = new Set();
            this._searchResults.forEach((node) => {
                if (!(node instanceof HTMLElement) || !node.isConnected) return;
                const parent = node.parentNode;
                if (!parent) return;
                parent.replaceChild(document.createTextNode(node.textContent || ''), node);
                normalizeTargets.add(parent);
            });
            normalizeTargets.forEach((node) => {
                if (typeof node.normalize === 'function') node.normalize();
            });
            this._searchResults = [];
            this._searchIndex = -1;
        }

        updateSearchResultState() {
            this._searchResults.forEach((node, index) => {
                node.classList.toggle('current-search-hit', index === this._searchIndex);
                this.applySearchHighlightStyle(node, index === this._searchIndex);
            });
        }

        getSearchHighlightPalette() {
            const rootStyle = getComputedStyle(document.documentElement);
            return {
                bg: rootStyle.getPropertyValue('--search-hit-bg').trim() || '#fff59d',
                text: rootStyle.getPropertyValue('--search-hit-text').trim() || '#111827',
                currentBg: rootStyle.getPropertyValue('--search-hit-current-bg').trim() || '#f7c948',
                currentText: rootStyle.getPropertyValue('--search-hit-current-text').trim() || '#111827',
                outline: rootStyle.getPropertyValue('--search-hit-outline').trim() || 'rgba(161, 98, 7, 0.18)',
                shadow: rootStyle.getPropertyValue('--search-hit-shadow').trim() || 'rgba(245, 158, 11, 0.24)'
            };
        }

        applySearchHighlightStyle(node, isCurrent = false) {
            if (!(node instanceof HTMLElement)) return;
            const palette = this.getSearchHighlightPalette();
            node.style.display = 'inline';
            node.style.padding = '0 1px';
            node.style.borderRadius = '3px';
            node.style.background = isCurrent ? palette.currentBg : palette.bg;
            node.style.color = isCurrent ? palette.currentText : palette.text;
            node.style.boxShadow = isCurrent
                ? `0 0 0 2px ${palette.shadow}`
                : `inset 0 0 0 1px ${palette.outline}`;
            node.style.boxDecorationBreak = 'clone';
            node.style.webkitBoxDecorationBreak = 'clone';
        }

        runSearch(keyword) {
            this.clearSearchHighlights();
            this._searchKeyword = (keyword || '').trim().toLowerCase();
            if (!this._searchKeyword) {
                this.updateSearchStatus();
                return;
            }
            const roots = this._details.flatMap((detail) => {
                const nodes = [];
                if (detail?.answerRoot) nodes.push(detail.answerRoot);
                if (detail?.questionRoot) nodes.push(detail.questionRoot);
                return nodes;
            });
            const seenRoots = new Set();
            roots.forEach((root) => {
                if (!root || seenRoots.has(root)) return;
                seenRoots.add(root);
                const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
                    acceptNode: (node) => {
                        const text = node.nodeValue || '';
                        if (!text.trim()) return NodeFilter.FILTER_REJECT;
                        const parent = node.parentElement;
                        if (!parent) return NodeFilter.FILTER_REJECT;
                        if (parent.closest('.search-hit')) return NodeFilter.FILTER_REJECT;
                        if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT', 'OPTION'].includes(parent.tagName)) {
                            return NodeFilter.FILTER_REJECT;
                        }
                        return text.toLowerCase().includes(this._searchKeyword)
                            ? NodeFilter.FILTER_ACCEPT
                            : NodeFilter.FILTER_REJECT;
                    }
                });
                const textNodes = [];
                let currentNode = walker.nextNode();
                while (currentNode) {
                    textNodes.push(currentNode);
                    currentNode = walker.nextNode();
                }
                textNodes.forEach((textNode) => {
                    const rawText = textNode.nodeValue || '';
                    const lowerText = rawText.toLowerCase();
                    const matchLength = this._searchKeyword.length;
                    let startIndex = 0;
                    let matchIndex = lowerText.indexOf(this._searchKeyword, startIndex);
                    if (matchIndex === -1 || !textNode.parentNode) return;
                    const fragment = document.createDocumentFragment();
                    while (matchIndex !== -1) {
                        if (matchIndex > startIndex) {
                            fragment.appendChild(document.createTextNode(rawText.slice(startIndex, matchIndex)));
                        }
                        const hit = document.createElement('span');
                        hit.className = 'search-hit';
                        hit.textContent = rawText.slice(matchIndex, matchIndex + matchLength);
                        this.applySearchHighlightStyle(hit, false);
                        fragment.appendChild(hit);
                        this._searchResults.push(hit);
                        startIndex = matchIndex + matchLength;
                        matchIndex = lowerText.indexOf(this._searchKeyword, startIndex);
                    }
                    if (startIndex < rawText.length) {
                        fragment.appendChild(document.createTextNode(rawText.slice(startIndex)));
                    }
                    textNode.parentNode.replaceChild(fragment, textNode);
                });
            });
            if (this._searchResults.length) {
                this._searchIndex = 0;
                this.updateSearchResultState();
                scrollToNode(this._searchResults[0], 'center');
            }
            this.updateSearchStatus();
        }

        stepSearch(step) {
            if (!this._searchResults.length) return;
            this._searchIndex = (this._searchIndex + step + this._searchResults.length) % this._searchResults.length;
            this.updateSearchResultState();
            scrollToNode(this._searchResults[this._searchIndex], 'center');
            this.updateSearchStatus();
        }

        updateSearchStatus() {
            if (!this.searchStatusEl) return;
            if (!this._searchKeyword) {
                this.searchStatusEl.textContent = '';
                return;
            }
            if (!this._searchResults.length) {
                this.searchStatusEl.textContent = '未找到结果';
                return;
            }
            this.searchStatusEl.textContent = `${this._searchIndex + 1} / ${this._searchResults.length}`;
        }

        normalizeMarkdown(md) {
            return (md || '')
                .replace(/\r\n/g, '\n')
                .replace(/\u00a0/g, ' ')
                .replace(/\n{3,}/g, '\n\n')
                .replace(/[ \t]+\n/g, '\n')
                .trim();
        }

        escapeMarkdownText(text) {
            return (text || '').replace(/\\/g, '\\\\');
        }

        escapeCodeFence(text) {
            return (text || '').replace(/```/g, '``\\`');
        }

        getCodeLanguage(preEl) {
            const code = preEl.querySelector('code');
            if (!code) return '';
            const className = code.className || '';
            const langMatch = className.match(/language-([a-zA-Z0-9_-]+)/);
            return langMatch ? langMatch[1] : '';
        }

        getOrderedListPrefix(li, listEl) {
            const start = parseInt(listEl.getAttribute('start') || '1', 10);
            const items = Array.from(listEl.children).filter(child => child.tagName === 'LI');
            const index = items.indexOf(li);
            return `${start + Math.max(index, 0)}. `;
        }

        convertTableToMarkdown(tableEl) {
            const rows = Array.from(tableEl.querySelectorAll('tr'))
                .map(row => Array.from(row.children)
                    .filter(cell => ['TH', 'TD'].includes(cell.tagName))
                    .map(cell => this.nodeToMarkdown(cell).replace(/\n+/g, '<br>').trim()))
                .filter(row => row.length > 0);

            if (!rows.length) return '';

            const header = rows[0];
            const body = rows.slice(1);
            const separator = header.map(() => '---');
            const lines = [
                `| ${header.join(' | ')} |`,
                `| ${separator.join(' | ')} |`
            ];

            body.forEach(row => {
                const normalizedRow = header.map((_, index) => row[index] || '');
                lines.push(`| ${normalizedRow.join(' | ')} |`);
            });

            return `${lines.join('\n')}\n\n`;
        }

        nodeToMarkdown(node, context = {}) {
            if (!node) return '';
            if (node.nodeType === Node.TEXT_NODE) {
                return this.escapeMarkdownText(node.textContent || '');
            }
            if (node.nodeType !== Node.ELEMENT_NODE) return '';

            const tag = node.tagName.toLowerCase();
            if (['button', 'svg', 'path', 'noscript', 'style', 'script'].includes(tag)) return '';

            if (tag === 'pre') {
                const text = (node.innerText || '').trimEnd();
                const lang = this.getCodeLanguage(node);
                return text ? `\n\`\`\`${lang}\n${this.escapeCodeFence(text)}\n\`\`\`\n\n` : '';
            }

            if (tag === 'code') {
                if (node.closest('pre')) return '';
                const inlineCode = (node.textContent || '').replace(/`/g, '\\`');
                return inlineCode ? `\`${inlineCode}\`` : '';
            }

            if (tag === 'br') return '\n';
            if (tag === 'hr') return '\n---\n\n';
            if (tag === 'img') {
                const alt = node.getAttribute('alt') || 'image';
                const src = node.getAttribute('src') || '';
                return src ? `![${alt}](${src})` : alt;
            }
            if (tag === 'a') {
                const text = this.normalizeMarkdown(Array.from(node.childNodes).map(child => this.nodeToMarkdown(child, context)).join('')) || sanitizeText(node.textContent);
                const href = node.getAttribute('href') || '';
                return href ? `[${text}](${href})` : text;
            }
            if (/^h[1-6]$/.test(tag)) {
                const level = parseInt(tag[1], 10);
                const text = this.normalizeMarkdown(Array.from(node.childNodes).map(child => this.nodeToMarkdown(child, context)).join(''));
                return text ? `${'#'.repeat(level)} ${text}\n\n` : '';
            }
            if (tag === 'blockquote') {
                const text = this.normalizeMarkdown(Array.from(node.childNodes).map(child => this.nodeToMarkdown(child, context)).join(''));
                if (!text) return '';
                return text.split('\n').map(line => (line ? `> ${line}` : '>')).join('\n') + '\n\n';
            }
            if (tag === 'ul' || tag === 'ol') {
                const items = Array.from(node.children).filter(child => child.tagName === 'LI');
                const lines = items.map(li => {
                    const content = this.normalizeMarkdown(Array.from(li.childNodes).map(child => this.nodeToMarkdown(child, { inList: true })).join(''));
                    if (!content) return '';
                    const prefix = tag === 'ol' ? this.getOrderedListPrefix(li, node) : '- ';
                    return content.split('\n').map((line, index) => (index === 0 ? `${prefix}${line}` : `  ${line}`)).join('\n');
                }).filter(Boolean);
                return lines.length ? `${lines.join('\n')}\n\n` : '';
            }
            if (tag === 'table') {
                return this.convertTableToMarkdown(node);
            }

            const childrenContent = Array.from(node.childNodes).map(child => this.nodeToMarkdown(child, context)).join('');
            const normalized = this.normalizeMarkdown(childrenContent);

            if (['p', 'div', 'section', 'article', 'li'].includes(tag)) {
                if (!normalized) return '';
                return context.inList && tag === 'li' ? normalized : `${normalized}\n\n`;
            }

            return normalized;
        }

        collectChatGPTMessages() {
            const messageWrappers = Array.from(new Set(
                queryAllDeep(CHATGPT_MESSAGE_SELECTOR).map(getClosestMessageWrapper).filter(Boolean)
            )).sort((a, b) => {
                if (a === b) return 0;
                const pos = a.compareDocumentPosition(b);
                if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
                if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
                return 0;
            });

            return messageWrappers.map((child, index) => {
                const roleEl = child.querySelector(CHATGPT_MESSAGE_SELECTOR) || child;
                if (!roleEl) return null;

                const role = roleEl.getAttribute('data-message-author-role');
                const contentRoot = role === 'assistant'
                    ? (child.querySelector('.markdown, [data-message-id] .markdown, .prose') || roleEl)
                    : roleEl;
                const markdown = this.normalizeMarkdown(this.nodeToMarkdown(contentRoot));

                return markdown ? {
                    index: index + 1,
                    role: role === 'user' ? 'User' : 'ChatGPT',
                    markdown
                } : null;
            }).filter(Boolean);
        }

        collectConversationMessages() {
            return this.collectChatGPTMessages();
        }

        buildSelectedExportMessages() {
            const activeIds = this._selectedQuestionIds.size
                ? new Set(this._selectedQuestionIds)
                : new Set(this._allQuestionIds);
            const messages = [];
            this._allQuestionIds.forEach((id, index) => {
                if (!activeIds.has(id)) return;
                const detail = this._allDetails[index] || {};
                const questionRoot = detail.questionRoot || this._allElements[index];
                const answerRoot = detail.answerRoot;
                const questionMarkdown = this.normalizeMarkdown(this.nodeToMarkdown(questionRoot));
                if (questionMarkdown) messages.push({ index: messages.length + 1, role: '问题', markdown: questionMarkdown });
                const answerMarkdown = this.normalizeMarkdown(this.nodeToMarkdown(answerRoot));
                if (answerMarkdown) messages.push({ index: messages.length + 1, role: '回答', markdown: answerMarkdown });
            });
            return messages;
        }

        getConversationExportMeta() {
            const rawTitle = document.title
                .replace(/ - ChatGPT$/i, '')
                .replace(/ - Gemini$/i, '')
                .replace(/[\\/:*?"<>|]/g, '_')
                .trim();
            return {
                title: rawTitle || `${getSiteTitle()} Conversation`,
                date: new Date().toLocaleString()
            };
        }

        exportToMarkdown() {
            const messages = this.buildSelectedExportMessages();
            if (messages.length === 0) return null;

            const { title, date } = this.getConversationExportMeta();
            let mdContent = `# ${title}\n\n*导出时间: ${date}*\n\n---\n\n`;

            messages.forEach(msg => {
                mdContent += `## ${msg.index}. ${msg.role}\n\n${msg.markdown}\n\n---\n\n`;
            });

            return { title, content: mdContent };
        }

        exportConversation() {
            const payload = this.exportToMarkdown();
            if (!payload) {
                alert(`未提取到有效的 ${getSiteTitle()} 对话内容,请确认页面已完整加载。`);
                return;
            }
            this.showExportPreview(payload.content);
        }

        showExportPreview(mdContent) {
            const overlay = document.createElement('div');
            overlay.style.cssText = 'position:fixed;inset:0;z-index:2147483647;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;';

            const dialog = document.createElement('div');
            dialog.style.cssText = 'width:min(720px,90vw);max-height:85vh;display:flex;flex-direction:column;background:#fff;border-radius:16px;box-shadow:0 24px 64px rgba(0,0,0,0.25);overflow:hidden;';

            const dialogHeader = document.createElement('div');
            dialogHeader.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid rgba(0,0,0,0.08);flex-shrink:0;';

            const dialogTitle = document.createElement('div');
            dialogTitle.style.cssText = 'font-size:15px;font-weight:700;color:#1f2937;';
            dialogTitle.textContent = '导出预览';

            const btnRow = document.createElement('div');
            btnRow.style.cssText = 'display:flex;gap:8px;';

            const copyBtn = document.createElement('button');
            copyBtn.textContent = '复制全文';
            copyBtn.style.cssText = 'padding:6px 16px;border:1px solid rgba(15,23,42,0.1);border-radius:8px;background:linear-gradient(180deg,#8dc887,#72b46e);color:#fff;font-size:13px;font-weight:600;cursor:pointer;';

            const downloadBtn = document.createElement('button');
            downloadBtn.textContent = '立即下载';
            downloadBtn.style.cssText = 'padding:6px 16px;border:1px solid rgba(15,23,42,0.1);border-radius:8px;background:linear-gradient(180deg,#60a5fa,#3b82f6);color:#fff;font-size:13px;font-weight:600;cursor:pointer;';

            const closeBtn = document.createElement('button');
            closeBtn.textContent = '✕';
            closeBtn.style.cssText = 'width:32px;height:32px;border:1px solid rgba(15,23,42,0.08);border-radius:8px;background:#fff;color:#6b7280;font-size:16px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;';

            btnRow.append(copyBtn, downloadBtn, closeBtn);
            dialogHeader.append(dialogTitle, btnRow);

            const pre = document.createElement('pre');
            pre.style.cssText = 'margin:0;padding:20px;overflow:auto;flex:1;font-family:"Cascadia Mono","Consolas","SFMono-Regular",monospace;font-size:13px;line-height:1.0;color:#1f2937;white-space:pre-wrap;word-break:break-word;background:#fafbfc;user-select:text;';
            pre.textContent = mdContent
                .replace(/\n{3,}/g, '\n')
                .replace(/^---$/gm, '───');

            dialog.append(dialogHeader, pre);
            overlay.appendChild(dialog);
            this.shadowRoot.appendChild(overlay);

            const close = () => { if (overlay.parentNode) overlay.remove(); };
            closeBtn.onclick = close;
            overlay.onclick = (e) => { if (e.target === overlay) close(); };

            copyBtn.onclick = async () => {
                try {
                    await copyText(mdContent);
                    copyBtn.textContent = '已复制 ✓';
                    setTimeout(() => { copyBtn.textContent = '复制全文'; }, 1500);
                } catch {
                    copyBtn.textContent = '复制失败';
                    setTimeout(() => { copyBtn.textContent = '复制全文'; }, 1500);
                }
            };

            downloadBtn.onclick = () => {
                const meta = this.getConversationExportMeta();
                const blob = new Blob([mdContent], { type: 'text/markdown' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `${meta.title}.md`;
                a.click();
                URL.revokeObjectURL(url);
            };
        }

        set questions(data) {
            this._allQuestions = data.questions;
            this._allElements = data.elements;
            this._allDetails = data.details || data.questions.map(() => ({ headings: [] }));
            this._allQuestionIds = data.ids || data.questions.map((_, index) => `q_${index}`);
            this._questions = this._allQuestions;
            this._elements = this._allElements;
            this._details = this._allDetails;
            this._questionIds = this._allQuestionIds;

            // 清理已选问题ID,只保留在新数据中仍然存在的ID(修复导出选中数据Bug)
            const validIds = new Set(this._allQuestionIds);
            this._selectedQuestionIds.forEach(id => {
                if (!validIds.has(id)) {
                    this._selectedQuestionIds.delete(id);
                }
            });

            this.updateList();
        }

        updateOpenState() {
            if (this._isOpen) this.setAttribute('open', '');
            else this.removeAttribute('open');
            if (this.eyeBtnEl) {
                this.eyeBtnEl.dataset.active = this._isOpen ? 'true' : 'false';
                this.eyeBtnEl.title = this._isOpen ? '收起导航' : '展开导航';
                this.eyeBtnEl.textContent = this._isOpen ? '👁️' : '👁️';
            }
        }

        render() {
            const style = document.createElement('style');
            style.textContent = cssText;

            const container = document.createElement('div');
            const shell = document.createElement('div');
            shell.className = 'app-shell';

            const panel = document.createElement('div');
            panel.className = 'panel';

            const header = document.createElement('div');
            header.className = 'header';

            const titleBlock = document.createElement('div');
            titleBlock.className = 'title-block';

            const title = document.createElement('div');
            title.className = 'title';
            title.textContent = '问题导航';

            const subtitle = document.createElement('div');
            subtitle.className = 'subtitle';
            subtitle.textContent = `${getSiteTitle()} 会话目录`;

            const headerActions = document.createElement('div');
            headerActions.className = 'header-actions';

            const exportBtn = document.createElement('button');
            exportBtn.className = 'icon-btn toggle-export';
            exportBtn.type = 'button';
            exportBtn.title = '导出 Markdown';
            exportBtn.textContent = '↓';

            const eyeBtn = document.createElement('button');
            eyeBtn.className = 'icon-btn toggle-eye';
            eyeBtn.type = 'button';
            eyeBtn.title = '收起导航';
            eyeBtn.textContent = '👁️';
            eyeBtn.dataset.active = 'true';

            const content = document.createElement('div');
            content.className = 'content';

            const listContainer = document.createElement('div');
            listContainer.className = 'list-container';

            const listEl = document.createElement('ul');
            listEl.id = 'question-list';

            const searchBar = document.createElement('div');
            searchBar.className = 'search-bar';
            const searchInput = document.createElement('input');
            searchInput.type = 'text';
            searchInput.className = 'text-input';
            searchInput.placeholder = '搜索问题或回答';
            const searchPrevBtn = document.createElement('button');
            searchPrevBtn.type = 'button';
            searchPrevBtn.className = 'icon-mini-btn';
            searchPrevBtn.title = '上一个';
            searchPrevBtn.textContent = '↑';
            const searchNextBtn = document.createElement('button');
            searchNextBtn.type = 'button';
            searchNextBtn.className = 'icon-mini-btn';
            searchNextBtn.title = '下一个';
            searchNextBtn.textContent = '↓';
            searchBar.append(searchInput, searchPrevBtn, searchNextBtn);

            const questionToolbar = document.createElement('div');
            questionToolbar.className = 'toolbar-row stack';
            const questionToolbarSummary = document.createElement('div');
            questionToolbarSummary.className = 'toolbar-summary';
            const searchStatus = document.createElement('div');
            searchStatus.className = 'helper-text';
            searchStatus.textContent = '';
            const questionSelectAll = document.createElement('label');
            questionSelectAll.className = 'master-check';
            const questionSelectAllInput = document.createElement('input');
            questionSelectAllInput.type = 'checkbox';
            questionSelectAllInput.className = 'select-check';
            const questionSelectAllText = document.createElement('span');
            questionSelectAllText.textContent = '全选';
            questionSelectAll.append(questionSelectAllInput, questionSelectAllText);
            const clearQuestionSelectionBtn = document.createElement('button');
            clearQuestionSelectionBtn.type = 'button';
            clearQuestionSelectionBtn.className = 'compact-btn';
            clearQuestionSelectionBtn.textContent = '取消选择';
            const questionToolbarHead = document.createElement('div');
            questionToolbarHead.className = 'toolbar-head';
            questionToolbarHead.append(questionSelectAll, clearQuestionSelectionBtn);
            questionToolbarSummary.append(searchStatus);
            questionToolbar.append(searchBar, questionToolbarHead, questionToolbarSummary);

            const fadeBottom = document.createElement('div');
            fadeBottom.className = 'fade-bottom';

            titleBlock.append(title, subtitle);
            listContainer.append(questionToolbar, listEl, fadeBottom);
            content.append(listContainer);
            headerActions.append(exportBtn, eyeBtn);
            header.append(titleBlock, headerActions);
            panel.append(header, content);
            shell.append(panel);
            container.appendChild(shell);

            this.eyeBtnEl = eyeBtn;
            this.searchInputEl = searchInput;
            this.searchStatusEl = searchStatus;
            this.questionSelectAllEl = questionSelectAllInput;

            eyeBtn.onclick = (event) => {
                if (this._suppressEyeClick) {
                    event.stopPropagation();
                    return;
                }
                if (this._isOpen) {
                    this.anchorToRight();
                }
                this._isOpen = !this._isOpen;
                this.updateOpenState();
            };

            exportBtn.onclick = (event) => {
                this.exportConversation();
            };

            questionSelectAllInput.onchange = () => this.setVisibleQuestionSelection(questionSelectAllInput.checked);
            clearQuestionSelectionBtn.onclick = () => this.clearQuestionSelection();
            searchInput.oninput = () => this.runSearch(searchInput.value);
            searchPrevBtn.onclick = () => this.stepSearch(-1);
            searchNextBtn.onclick = () => this.stepSearch(1);

            listEl.onclick = (event) => {
                const li = event.target.closest('li');
                if (!li) return;
                const idx = Number(li.dataset.index);
                if (idx < 0) return;
                const targetEl = this._elements[idx];
                if (!targetEl) return;
                if (!isChatGPTSharePage()) targetEl.style.scrollMarginTop = '56px';
                targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
            };

            this.shadowRoot.append(style, container);
            this.listEl = listEl;
            this.updateOpenState();
        }

        updateList() {
            if (!this.listEl) return;
            this.listEl.replaceChildren();
            const visibleIds = this.getVisibleQuestionIds();
            if (this.questionSelectAllEl) {
                this.questionSelectAllEl.checked = !!visibleIds.length && visibleIds.every((id) => this._selectedQuestionIds.has(id));
            }
            if (this._questions.length === 0) {
                const placeholder = '暂未识别到问题';
                const li = document.createElement('li');
                li.dataset.index = '-1';
                li.title = placeholder;
                li.textContent = placeholder;
                this.listEl.appendChild(li);
                this.updateActiveItem();
                return;
            }
            this._questions.forEach((question, index) => {
                const li = document.createElement('li');
                li.className = 'question-item';
                li.dataset.index = String(index);

                const row = document.createElement('div');
                row.className = 'question-row';
                row.title = question;

                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.className = 'select-check';
                checkbox.checked = this._selectedQuestionIds.has(this._questionIds[index]);
                checkbox.onclick = (event) => {
                    event.stopPropagation();
                    this.toggleQuestionSelection(this._questionIds[index], checkbox.checked);
                };

                const text = document.createElement('div');
                text.className = 'question-text';
                text.textContent = `问题:${question}`;

                const detail = this._details[index];
                const hasHeadings = !!detail?.headings?.length || !!detail?.answerRoot;
                const expandBtn = document.createElement('button');
                expandBtn.type = 'button';
                expandBtn.className = 'expand-btn';
                expandBtn.textContent = hasHeadings ? (this._expandedQuestions.has(index) ? '▼' : '▶') : '·';
                expandBtn.disabled = !hasHeadings;

                const answerTree = document.createElement('div');
                answerTree.className = 'answer-tree';
                answerTree.style.display = this._expandedQuestions.has(index) ? 'flex' : 'none';

                if (this._expandedQuestions.has(index) && hasHeadings) {
                    if (!detail.headings?.length && detail?.answerRoot) {
                        const child = document.createElement('div');
                        child.className = 'answer-node';
                        child.style.paddingLeft = '10px';
                        child.textContent = '回答';
                        child.onclick = (event) => {
                            event.stopPropagation();
                            scrollToNode(detail.answerRoot, 'start');
                        };
                        answerTree.appendChild(child);
                    }
                    detail.headings.forEach((item) => {
                        const child = document.createElement('div');
                        child.className = 'answer-node';
                        child.style.paddingLeft = `${10 + Math.max(item.level - 1, 0) * 14}px`;
                        child.textContent = `${item.level === 1 ? '回答:' : ''}${item.text}`;
                        child.onclick = (event) => {
                            event.stopPropagation();
                            scrollToNode(item.target, 'start');
                        };
                        answerTree.appendChild(child);
                    });
                }

                expandBtn.onclick = (event) => {
                    event.stopPropagation();
                    if (!hasHeadings) return;
                    if (this._expandedQuestions.has(index)) {
                        this._expandedQuestions.delete(index);
                    } else {
                        this._expandedQuestions.add(index);
                    }
                    this.updateList();
                };

                row.onclick = () => {
                    const targetEl = this._elements[index];
                    if (!targetEl) return;
                    scrollToNode(targetEl, 'start');
                };

                row.append(checkbox, text, expandBtn);
                li.append(row, answerTree);
                this.listEl.appendChild(li);
            });
            this.updateActiveItem();
        }

        updateActiveItem() {
            if (!this.listEl) return;
            const lis = this.listEl.querySelectorAll('.question-item');
            lis.forEach((li, index) => {
                if (index === activeIndex) li.classList.add('active');
                else li.classList.remove('active');
            });
        }

        startTracking() {
            const scrollTarget = queryScrollContainer();
            if (!scrollTarget) return;
            scrollTarget.addEventListener('scroll', () => {
                requestAnimationFrame(() => this.syncActiveIndex());
            }, { passive: true });
        }

        syncActiveIndex() {
            const newIndex = this._elements.findIndex(el => el.getBoundingClientRect().top >= 0);
            if (newIndex !== -1 && newIndex !== activeIndex) {
                activeIndex = newIndex;
                this.updateActiveItem();
            }
        }

    }

    customElements.define('ai-conversation-navigator', NavigatorApp);
    // [END: NavigatorApp 组件]

    // ============================================================
    // ==Main== 入口层 - 初始化与事件绑定
    // ============================================================

    // [START: 入口函数]
    function extractQuestionText(element) {
        if (!element) return '';
        return sanitizeText(element.querySelector('[data-message-author-role="user"]')?.innerText || element.innerText);
    }

    function collectHeadingTargets(root) {
        if (!root) return [];
        const headingNodes = Array.from(root.querySelectorAll('h1, h2, h3, h4, h5, h6'))
            .filter(node => sanitizeText(node.textContent).length > 0);
        return headingNodes.map(node => ({
            text: sanitizeText(node.textContent),
            level: Number(node.tagName.slice(1)),
            target: node
        }));
    }

    function getChatGPTNavigatorEntries() {
        const wrappers = Array.from(new Set(
            queryAllDeep(CHATGPT_MESSAGE_SELECTOR).map(getClosestMessageWrapper).filter(Boolean)
        )).sort((a, b) => {
            if (a === b) return 0;
            const pos = a.compareDocumentPosition(b);
            if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
            if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
            return 0;
        });

        const entries = [];
        wrappers.forEach((wrapper) => {
            const roleEl = wrapper.querySelector(CHATGPT_MESSAGE_SELECTOR) || wrapper;
            const role = roleEl?.getAttribute('data-message-author-role');
            if (role !== 'user') return;

            entries.push({
                id: wrapper.dataset.messageId || wrapper.getAttribute('data-testid') || `chatgpt_${entries.length + 1}`,
                question: extractQuestionText(wrapper),
                element: wrapper,
                detail: { headings: [], questionRoot: wrapper, answerRoot: null }
            });
        });

        let entryIndex = -1;
        wrappers.forEach((wrapper) => {
            const roleEl = wrapper.querySelector(CHATGPT_MESSAGE_SELECTOR) || wrapper;
            const role = roleEl?.getAttribute('data-message-author-role');
            if (role === 'user') {
                entryIndex += 1;
                return;
            }
            if (role === 'assistant' && entries[entryIndex]) {
                const contentRoot = wrapper.querySelector('.markdown, [data-message-id] .markdown, .prose') || wrapper;
                entries[entryIndex].detail.headings = collectHeadingTargets(contentRoot);
                entries[entryIndex].detail.answerRoot = contentRoot;
            }
        });

        return entries.filter(item => item.question);
    }

    function collectNavigatorEntries() {
        return getChatGPTNavigatorEntries();
    }

    function ensureNavigatorApp() {
        if (!document.body) return null;
        let app = document.getElementById(DOM_MARK);
        if (!app) {
            app = document.createElement('ai-conversation-navigator');
            app.id = DOM_MARK;
            document.body.appendChild(app);
        }
        return app;
    }

    async function updateNavigator() {
        if (!loaded) {
            await ensureFullDOMRendered();
        }
        const entries = collectNavigatorEntries();
        const elements = entries.map(item => item.element).filter(Boolean);
        let app = document.getElementById(DOM_MARK);

        if (elements.length > 0) {
            app = app || ensureNavigatorApp();
            if (!app) return;

            const filteredEntries = entries.filter(item => item.question && item.element);
            app.questions = {
                ids: filteredEntries.map(item => item.id),
                questions: filteredEntries.map(item => item.question),
                elements: filteredEntries.map(item => item.element),
                details: filteredEntries.map(item => item.detail || { headings: [] })
            };
        } else if (app) {
            app.remove();
        }
    }

    const refreshNavigator = async () => {
        const latestKey = getConversationKey();
        if (conversationKey !== latestKey) {
            conversationKey = latestKey;
            loaded = false;
            activeIndex = null;
            const app = document.getElementById(DOM_MARK);
            if (app) app.remove();
        }

        const elements = queryQuestionElements();
        if (elements.length > 0) {
            await updateNavigator();
            loaded = true;
        } else {
            const app = document.getElementById(DOM_MARK);
            if (app) app.remove();
            loaded = false;
        }
    };

    const scheduleRefreshNavigator = () => {
        if (refreshScheduled) return;
        refreshScheduled = true;
        requestAnimationFrame(async () => {
            refreshScheduled = false;
            await refreshNavigator();
        });
    };

    function startChatGPTBootstrap() {
        setInterval(scheduleRefreshNavigator, 800);

        const observer = new MutationObserver(() => {
            scheduleRefreshNavigator();
        });

        observer.observe(document.documentElement, {
            childList: true,
            subtree: true
        });
    }

    startChatGPTBootstrap();
    // [END: 入口函数]

})();