// ==UserScript== // @name 通用网页AI速读 // @namespace http://tampermonkey.net/ // @version 2.0 // @description 在任意网页上使用AI总结和对话功能 // @author jaleeffy // @match *://*/* // @icon data:image/svg+xml,💡 // @require https://cdn.jsdelivr.net/npm/marked/marked.min.js // @require https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js // @grant GM_setValue // @grant GM_getValue // @grant GM_setClipboard // @license MIT // ==/UserScript== (function() { 'use strict'; const STYLES = ` @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); :host { font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --primary: #52a67d; --primary-light: #6bc299; --primary-dark: #3d8a64; --primary-gradient: linear-gradient(135deg, #52a67d 0%, #6bc299 50%, #5bb88a 100%); --primary-glow: rgba(82, 166, 125, 0.35); --success: #2d9d78; --success-light: #d1fae5; --danger: #dc4446; --danger-light: #fef2f2; --warning: #d4a054; --bg-base: #f5f9f7; --bg-card: #ffffff; --bg-glass: rgba(255, 255, 255, 0.88); --bg-glass-dark: rgba(245, 249, 247, 0.96); --bg-hover: rgba(82, 166, 125, 0.08); --bg-active: rgba(82, 166, 125, 0.12); --bg-setting: #eff5f2; --bg-input: #ffffff; --border-light: rgba(82, 166, 125, 0.12); --border-medium: rgba(82, 166, 125, 0.2); --shadow-sm: 0 1px 3px rgba(61, 138, 100, 0.06), 0 1px 2px rgba(61, 138, 100, 0.08); --shadow-md: 0 4px 12px -2px rgba(61, 138, 100, 0.12), 0 2px 6px -2px rgba(61, 138, 100, 0.08); --shadow-lg: 0 12px 40px -8px rgba(61, 138, 100, 0.18), 0 4px 12px -4px rgba(61, 138, 100, 0.08); --shadow-xl: 0 20px 50px -12px rgba(61, 138, 100, 0.25), 0 0 0 1px rgba(82, 166, 125, 0.05); --shadow-glow: 0 4px 20px var(--primary-glow), 0 0 0 1px rgba(82, 166, 125, 0.1); --text-main: #1f3a2e; --text-sec: #4a6456; --text-muted: #7a9488; --text-inverse: #ffffff; --sidebar-width: 420px; --btn-size: 26px; --btn-height: 35px; --radius-sm: 10px; --radius-md: 14px; --radius-lg: 18px; --radius-xl: 24px; --radius-full: 9999px; --transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1); --transition-normal: 0.25s cubic-bezier(0.4, 0, 0.2, 1); --transition-slow: 0.35s cubic-bezier(0.4, 0, 0.2, 1); } :host(.dark-theme) { --bg-base: #1a1f1c; --bg-card: #232a26; --bg-glass: rgba(35, 42, 38, 0.92); --bg-glass-dark: rgba(26, 31, 28, 0.96); --bg-hover: rgba(107, 194, 153, 0.12); --bg-active: rgba(107, 194, 153, 0.18); --bg-setting: #1e2421; --bg-input: #2a3330; --border-light: rgba(107, 194, 153, 0.15); --border-medium: rgba(107, 194, 153, 0.25); --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2); --shadow-md: 0 4px 12px -2px rgba(0, 0, 0, 0.3); --shadow-lg: 0 12px 40px -8px rgba(0, 0, 0, 0.4); --shadow-xl: 0 20px 50px -12px rgba(0, 0, 0, 0.5); --text-main: #e8f5ef; --text-sec: #a8c9b8; --text-muted: #6d8a7a; } * { box-sizing: border-box; } .sidebar-panel { position: fixed; top: 0; bottom: 0; width: var(--sidebar-width); background: var(--bg-glass-dark); backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%); box-shadow: var(--shadow-xl); z-index: 9998; display: flex; flex-direction: column; transition: transform var(--transition-slow); border: 1px solid var(--border-light); } .panel-left { left: 0; border-left: none; border-radius: 0 var(--radius-xl) var(--radius-xl) 0; transform: translateX(-100%); } .panel-left.open { transform: translateX(0); } .panel-right { right: 0; border-right: none; border-radius: var(--radius-xl) 0 0 var(--radius-xl); transform: translateX(100%); } .panel-right.open { transform: translateX(0); } #toggle-btn { position: fixed; width: var(--btn-size); height: var(--btn-height); background: var(--primary-gradient); color: white; box-shadow: var(--shadow-glow); z-index: 9999; cursor: grab; display: flex; align-items: center; justify-content: center; user-select: none; transition: all var(--transition-normal); border: 2px solid rgba(255, 255, 255, 0.2); outline: none; } #toggle-btn::before { content: ''; position: absolute; inset: -3px; border-radius: inherit; background: var(--primary-gradient); opacity: 0; z-index: -1; filter: blur(12px); transition: opacity var(--transition-normal); } #toggle-btn:hover { transform: scale(1.08); box-shadow: 0 8px 30px var(--primary-glow), 0 0 0 4px rgba(82, 166, 125, 0.15); } #toggle-btn:hover::before { opacity: 0.6; } #toggle-btn:active { cursor: grabbing; transform: scale(0.96); } #toggle-btn svg { width: 16px; height: 16px; fill: currentColor; transition: transform var(--transition-normal); filter: drop-shadow(0 2px 4px rgba(0,0,0,0.15)); } .btn-snap-left { border-radius: 0 var(--radius-md) var(--radius-md) 0; } .btn-snap-right { border-radius: var(--radius-md) 0 0 var(--radius-md); } .btn-floating { border-radius: var(--radius-md); } #toggle-btn.arrow-flip svg { transform: rotate(180deg); } .resize-handle { position: absolute; top: 0; bottom: 0; width: 6px; cursor: col-resize; z-index: 10001; background: transparent; transition: background var(--transition-fast); } .resize-handle::after { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 3px; height: 40px; background: var(--primary); border-radius: 2px; opacity: 0; transition: opacity var(--transition-fast); } .resize-handle:hover::after { opacity: 0.5; } .handle-left { right: -3px; } .handle-right { left: -3px; } .header { padding: 20px 24px; border-bottom: 1px solid var(--border-light); display: flex; justify-content: space-between; align-items: center; background: linear-gradient(to bottom, var(--bg-card), transparent); flex-shrink: 0; } .header-title { font-size: 18px; font-weight: 700; color: var(--text-main); display: flex; align-items: center; gap: 12px; letter-spacing: -0.02em; } .header-title-icon { width: 36px; height: 36px; background: var(--primary-gradient); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; font-size: 18px; box-shadow: var(--shadow-sm); } .header-actions { display: flex; gap: 6px; } .icon-btn { background: transparent; border: none; cursor: pointer; padding: 10px; border-radius: var(--radius-sm); color: var(--text-sec); transition: all var(--transition-fast); font-size: 16px; display: flex; align-items: center; justify-content: center; position: relative; } .icon-btn:hover { background: var(--bg-hover); color: var(--primary); transform: scale(1.05); } .icon-btn:active { transform: scale(0.95); } .icon-btn.active { background: var(--bg-active); color: var(--primary); } .icon-btn[data-tooltip]::after { content: attr(data-tooltip); position: absolute; bottom: -32px; left: 50%; transform: translateX(-50%) scale(0.9); background: var(--text-main); color: var(--text-inverse); padding: 5px 10px; border-radius: 6px; font-size: 11px; font-weight: 500; white-space: nowrap; opacity: 0; pointer-events: none; transition: all var(--transition-fast); z-index: 100; } .icon-btn[data-tooltip]:hover::after { opacity: 1; transform: translateX(-50%) scale(1); } .tab-bar { display: flex; padding: 12px 16px; gap: 6px; border-bottom: 1px solid var(--border-light); background: var(--bg-glass); flex-shrink: 0; } .tab-item { flex: 1; padding: 12px 16px; text-align: center; font-size: 13px; font-weight: 600; color: var(--text-sec); cursor: pointer; border-radius: var(--radius-sm); transition: all var(--transition-fast); display: flex; align-items: center; justify-content: center; gap: 8px; position: relative; overflow: hidden; } .tab-item::before { content: ''; position: absolute; inset: 0; background: var(--primary-gradient); opacity: 0; transition: opacity var(--transition-fast); } .tab-item:hover { color: var(--primary); background: var(--bg-hover); } .tab-item.active { color: var(--text-inverse); background: var(--primary-gradient); box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255,255,255,0.2); } .tab-item.active::before { opacity: 1; } .tab-item span { position: relative; z-index: 1; } .content-area { flex: 1; overflow-y: auto; position: relative; background: var(--bg-base); } .view-page { padding: 24px; display: none; animation: fadeSlideIn 0.35s ease; } .view-page.active { display: block; } @keyframes fadeSlideIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } .form-group { margin-bottom: 24px; } .form-label { display: block; font-size: 11px; color: var(--text-sec); margin-bottom: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; } input, textarea, select { width: 100%; padding: 14px 18px; border: 2px solid var(--border-light); border-radius: var(--radius-md); font-size: 14px; font-family: inherit; background: var(--bg-input); box-sizing: border-box; transition: all var(--transition-fast); color: var(--text-main); } input:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px rgba(82, 166, 125, 0.12); background: var(--bg-card); } input::placeholder, textarea::placeholder { color: var(--text-muted); } textarea { resize: vertical; min-height: 100px; line-height: 1.6; } .btn { width: 100%; padding: 16px 24px; border: none; border-radius: var(--radius-md); background: var(--primary-gradient); color: var(--text-inverse); font-weight: 600; font-size: 15px; font-family: inherit; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 10px; transition: all var(--transition-normal); box-shadow: var(--shadow-glow); letter-spacing: 0.02em; position: relative; overflow: hidden; } .btn::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); transition: left 0.5s; } .btn:hover { transform: translateY(-2px); box-shadow: 0 8px 30px var(--primary-glow); } .btn:hover::before { left: 100%; } .btn:active { transform: translateY(0) scale(0.98); } .btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; } .btn-secondary { background: var(--bg-card); color: var(--text-main); box-shadow: var(--shadow-sm); border: 2px solid var(--border-light); } .btn-secondary:hover { background: var(--bg-hover); border-color: var(--primary); box-shadow: var(--shadow-md); } .btn-xs { padding: 8px 14px; font-size: 12px; font-family: inherit; background: var(--bg-card); color: var(--text-main); border-radius: var(--radius-sm); border: 1.5px solid var(--border-light); cursor: pointer; white-space: nowrap; font-weight: 600; transition: all var(--transition-fast); } .btn-xs:hover { background: var(--primary); color: var(--text-inverse); border-color: var(--primary); transform: translateY(-1px); } .result-box { margin-top: 20px; padding: 24px; background: var(--bg-card); border: 1px solid var(--border-light); border-radius: var(--radius-lg); font-size: 14px; line-height: 1.8; color: var(--text-main); min-height: 180px; max-height: calc(100vh - 380px); overflow-y: auto; word-break: break-word; box-shadow: var(--shadow-sm); position: relative; } .result-box::-webkit-scrollbar { width: 4px; } .result-box::-webkit-scrollbar-track { background: transparent; } .result-box::-webkit-scrollbar-thumb { background: rgba(156, 139, 128, 0.3); border-radius: 2px; } .result-box::-webkit-scrollbar-thumb:hover { background: rgba(156, 139, 128, 0.5); } .result-box.empty { display: flex; align-items: center; justify-content: center; color: var(--text-muted); font-size: 13px; text-align: center; background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-base) 100%); } .result-actions { position: absolute; top: 12px; right: 12px; display: flex; gap: 6px; opacity: 0; transition: opacity var(--transition-fast); } .result-box:hover .result-actions { opacity: 1; } .result-action-btn { padding: 6px 12px; font-size: 11px; font-family: inherit; background: var(--bg-glass); color: var(--text-sec); border: 1px solid var(--border-light); border-radius: var(--radius-sm); cursor: pointer; display: flex; align-items: center; gap: 4px; transition: all var(--transition-fast); backdrop-filter: blur(8px); } .result-action-btn:hover { background: var(--primary); color: var(--text-inverse); border-color: var(--primary); } .result-action-btn.copied { background: var(--success); color: var(--text-inverse); border-color: var(--success); } .result-box h1, .result-box h2, .result-box h3 { margin: 20px 0 12px; font-weight: 700; color: var(--text-main); letter-spacing: -0.02em; } .result-box h1 { font-size: 1.5em; } .result-box h2 { font-size: 1.25em; border-bottom: 2px solid var(--border-light); padding-bottom: 8px; } .result-box h3 { font-size: 1.1em; color: var(--primary); } .result-box p { margin-bottom: 14px; } .result-box ul, .result-box ol { padding-left: 24px; margin: 12px 0; } .result-box li { margin-bottom: 8px; } .result-box li::marker { color: var(--primary); } .result-box code { background: var(--bg-hover); padding: 3px 8px; border-radius: 6px; font-family: 'JetBrains Mono', 'SF Mono', monospace; color: var(--primary-dark); font-size: 0.88em; border: 1px solid var(--border-light); } .result-box pre { background: linear-gradient(135deg, #2d2520 0%, #3d332c 100%); padding: 18px; border-radius: var(--radius-md); overflow-x: auto; color: #f5f0eb; border: 1px solid rgba(255,255,255,0.1); } .result-box pre code { background: none; color: #f5f0eb; padding: 0; border: none; } .result-box blockquote { border-left: 4px solid var(--primary); margin: 16px 0; padding: 14px 20px; color: var(--text-sec); background: var(--bg-hover); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; font-style: italic; } .result-box a { color: var(--primary); text-decoration: none; border-bottom: 1px solid var(--primary-light); transition: all var(--transition-fast); } .result-box a:hover { color: var(--primary-dark); border-bottom-color: var(--primary-dark); } .result-box strong { color: var(--primary-dark); font-weight: 600; } .chat-container { display: flex; flex-direction: column; height: 100%; position: relative; } .chat-toolbar { display: flex; justify-content: space-between; align-items: center; padding-bottom: 14px; border-bottom: 1px solid var(--border-light); margin-bottom: 14px; } .chat-toolbar-title { font-size: 13px; color: var(--text-sec); font-weight: 600; display: flex; align-items: center; gap: 8px; } .chat-toolbar-title .msg-count { background: var(--primary-gradient); color: var(--text-inverse); font-size: 11px; padding: 3px 10px; border-radius: var(--radius-full); font-weight: 700; box-shadow: var(--shadow-sm); } .btn-clear { padding: 8px 14px; font-size: 12px; font-family: inherit; background: var(--danger-light); color: var(--danger); border-radius: var(--radius-sm); border: 1px solid transparent; cursor: pointer; font-weight: 600; transition: all var(--transition-fast); display: flex; align-items: center; gap: 5px; } .btn-clear:hover { background: var(--danger); color: var(--text-inverse); transform: scale(1.02); } .chat-messages-wrapper { flex: 1; position: relative; overflow: hidden; } .chat-messages { height: 100%; overflow-y: auto; padding: 16px 0; } .chat-list { display: flex; flex-direction: column; gap: 18px; } .bubble { padding: 16px 20px; border-radius: var(--radius-lg); font-size: 14px; line-height: 1.75; max-width: 88%; word-break: break-word; box-shadow: var(--shadow-sm); animation: bubbleIn 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); position: relative; } @keyframes bubbleIn { from { opacity: 0; transform: translateY(15px) scale(0.92); } to { opacity: 1; transform: translateY(0) scale(1); } } .bubble-user { align-self: flex-end; background: var(--primary-gradient); color: var(--text-inverse); border-bottom-right-radius: 6px; box-shadow: var(--shadow-glow); } .bubble-ai { align-self: flex-start; background: var(--bg-card); border: 1px solid var(--border-light); color: var(--text-main); border-bottom-left-radius: 6px; } .bubble-ai h1, .bubble-ai h2, .bubble-ai h3 { margin: 12px 0 8px; } .bubble-ai p { margin-bottom: 10px; } .bubble-ai p:last-child { margin-bottom: 0; } .bubble-ai code { background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: 'JetBrains Mono', monospace; font-size: 0.85em; } .bubble-ai .thinking-block { margin: -6px -8px 12px; border-radius: var(--radius-sm); } .bubble-ai .thinking-block:last-child { margin-bottom: -6px; } .bubble-ai .thinking-header { padding: 8px 12px; } .bubble-ai .thinking-preview { padding: 0 12px 10px; font-size: 11px; } .bubble-ai .thinking-content-inner { padding: 10px 12px 12px; font-size: 11px; max-height: 300px; } .scroll-buttons { position: absolute; right: 10px; display: flex; flex-direction: column; gap: 8px; z-index: 10; transition: all var(--transition-normal); } .scroll-buttons.top-area { top: 10px; } .scroll-buttons.bottom-area { bottom: 10px; } .scroll-btn { width: 36px; height: 36px; border-radius: var(--radius-full); background: var(--bg-card); border: 1px solid var(--border-light); box-shadow: var(--shadow-md); cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text-sec); transition: all var(--transition-fast); opacity: 0; transform: scale(0.8); pointer-events: none; } .scroll-btn.visible { opacity: 1; transform: scale(1); pointer-events: auto; } .scroll-btn:hover { background: var(--primary); color: var(--text-inverse); border-color: var(--primary); box-shadow: var(--shadow-glow); transform: scale(1.1); } .scroll-btn.generating { background: var(--primary-gradient); color: var(--text-inverse); border-color: var(--primary); box-shadow: var(--shadow-glow); animation: pulse-btn 1.5s ease-in-out infinite; } .scroll-btn.generating::after { content: '新内容'; position: absolute; right: 42px; background: var(--primary-gradient); color: white; font-size: 10px; font-weight: 600; padding: 4px 10px; border-radius: 12px; white-space: nowrap; box-shadow: var(--shadow-md); animation: fadeIn 0.3s ease; } @keyframes pulse-btn { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } } @keyframes fadeIn { from { opacity: 0; transform: translateX(10px); } to { opacity: 1; transform: translateX(0); } } .scroll-btn svg { width: 18px; height: 18px; fill: currentColor; } .chat-input-area { border-top: 1px solid var(--border-light); padding: 18px 0 0; flex-shrink: 0; background: linear-gradient(to top, var(--bg-base), transparent); } .chat-input-row { display: flex; gap: 14px; align-items: flex-end; } .chat-input { flex: 1; min-height: 52px; max-height: 140px; border-radius: var(--radius-xl); padding: 16px 22px; resize: none; border: 2px solid var(--border-light); font-size: 14px; line-height: 1.5; transition: all var(--transition-fast); } .chat-input:focus { border-color: var(--primary); box-shadow: 0 0 0 4px rgba(82, 166, 125, 0.12); } .chat-input:disabled { opacity: 0.6; cursor: not-allowed; background: var(--bg-setting); } .chat-input::placeholder { color: var(--text-muted); font-style: italic; } .thinking-block { margin-bottom: 16px; border-radius: var(--radius-md); overflow: hidden; background: linear-gradient(135deg, rgba(82, 166, 125, 0.05) 0%, rgba(82, 166, 125, 0.02) 100%); border: 1px solid rgba(82, 166, 125, 0.12); transition: all var(--transition-normal); } .thinking-block:hover { border-color: rgba(82, 166, 125, 0.22); box-shadow: 0 2px 12px rgba(82, 166, 125, 0.06); } .thinking-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; cursor: pointer; user-select: none; transition: background var(--transition-fast); } .thinking-header:hover { background: rgba(82, 166, 125, 0.05); } .thinking-header-left { display: flex; align-items: center; gap: 8px; } .thinking-icon { width: 24px; height: 24px; border-radius: 6px; background: var(--primary-gradient); display: flex; align-items: center; justify-content: center; font-size: 12px; box-shadow: var(--shadow-sm); flex-shrink: 0; } .thinking-title { font-size: 12px; font-weight: 600; color: var(--primary-dark); } .thinking-status { font-size: 10px; color: var(--text-muted); background: rgba(82, 166, 125, 0.1); padding: 2px 8px; border-radius: 10px; margin-left: 6px; } .thinking-block.streaming .thinking-status { background: var(--primary); color: white; animation: status-pulse 1.2s ease-in-out infinite; } @keyframes status-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } } .thinking-toggle { width: 22px; height: 22px; border-radius: 50%; background: rgba(82, 166, 125, 0.08); display: flex; align-items: center; justify-content: center; transition: all var(--transition-fast); flex-shrink: 0; } .thinking-toggle:hover { background: rgba(82, 166, 125, 0.15); } .thinking-toggle svg { width: 12px; height: 12px; fill: var(--primary); transition: transform var(--transition-normal); } .thinking-block.expanded .thinking-toggle svg { transform: rotate(180deg); } .thinking-preview { padding: 0 14px 12px; font-size: 12px; line-height: 1.5; color: var(--text-muted); max-height: 4.5em; overflow: hidden; position: relative; } .thinking-preview p { margin: 0 0 4px; font-size: 12px; } .thinking-preview p:last-child { margin-bottom: 0; } .thinking-preview::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 24px; background: linear-gradient(to bottom, transparent, rgba(253, 250, 247, 0.98)); pointer-events: none; } .thinking-block.expanded .thinking-preview { display: none; } .thinking-content { max-height: 0; overflow: hidden; transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1); } .thinking-block.expanded .thinking-content { max-height: 5000px; } .thinking-content-inner { padding: 12px 14px 14px; font-size: 12px; line-height: 1.7; color: var(--text-sec); border-top: 1px dashed rgba(82, 166, 125, 0.12); max-height: 400px; overflow-y: auto; } .thinking-content-inner p { margin-bottom: 8px; font-size: 12px; } .thinking-content-inner p:last-child { margin-bottom: 0; } .thinking-content-inner h1, .thinking-content-inner h2, .thinking-content-inner h3 { font-size: 13px; margin: 10px 0 6px; color: var(--primary-dark); } .thinking-content-inner ul, .thinking-content-inner ol { padding-left: 18px; margin: 6px 0; } .thinking-content-inner li { margin-bottom: 4px; font-size: 12px; } .thinking-content-inner code { font-family: 'JetBrains Mono', monospace; font-size: 11px; background: rgba(82, 166, 125, 0.08); padding: 1px 5px; border-radius: 3px; } .thinking-content-inner pre { background: rgba(45, 37, 32, 0.9); padding: 10px; border-radius: 6px; overflow-x: auto; font-size: 11px; } .thinking-content-inner pre code { background: none; padding: 0; } .thinking-content-inner::-webkit-scrollbar { width: 3px; } .thinking-content-inner::-webkit-scrollbar-track { background: transparent; } .thinking-content-inner::-webkit-scrollbar-thumb { background: rgba(82, 166, 125, 0.25); border-radius: 2px; } .thinking-content-inner::-webkit-scrollbar-thumb:hover { background: rgba(82, 166, 125, 0.4); } .thinking-block.streaming .thinking-icon { animation: pulse-glow 1.5s ease-in-out infinite; } @keyframes pulse-glow { 0%, 100% { box-shadow: var(--shadow-sm); } 50% { box-shadow: 0 0 10px var(--primary-glow); } } :host(.dark-theme) .thinking-block { background: linear-gradient(135deg, rgba(107, 194, 153, 0.06) 0%, rgba(107, 194, 153, 0.02) 100%); border-color: rgba(107, 194, 153, 0.15); } :host(.dark-theme) .thinking-header:hover { background: rgba(107, 194, 153, 0.06); } :host(.dark-theme) .thinking-title { color: var(--primary-light); } :host(.dark-theme) .thinking-toggle { background: rgba(107, 194, 153, 0.12); } :host(.dark-theme) .thinking-content-inner { border-top-color: rgba(107, 194, 153, 0.15); } :host(.dark-theme) .thinking-preview::after { background: linear-gradient(to bottom, transparent, rgba(35, 42, 38, 0.98)); } :host(.dark-theme) .content-area::-webkit-scrollbar-track { background: rgba(107, 194, 153, 0.05); } :host(.dark-theme) .result-box::-webkit-scrollbar-thumb { background: rgba(168, 201, 184, 0.25); } :host(.dark-theme) .result-box::-webkit-scrollbar-thumb:hover { background: rgba(168, 201, 184, 0.4); } :host(.dark-theme) .chat-messages::-webkit-scrollbar-thumb { background: rgba(168, 201, 184, 0.2); } :host(.dark-theme) .chat-messages::-webkit-scrollbar-thumb:hover { background: rgba(168, 201, 184, 0.35); } :host(.dark-theme) .thinking-content-inner::-webkit-scrollbar-thumb { background: rgba(107, 194, 153, 0.2); } :host(.dark-theme) .thinking-content-inner::-webkit-scrollbar-thumb:hover { background: rgba(107, 194, 153, 0.35); } .send-btn { width: 52px; height: 52px; border-radius: var(--radius-full); padding: 0; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: var(--primary-gradient); border: none; cursor: pointer; transition: all var(--transition-normal); box-shadow: var(--shadow-glow); } .send-btn:hover { transform: scale(1.08) rotate(5deg); box-shadow: 0 8px 30px var(--primary-glow); } .send-btn:active { transform: scale(0.95); } .send-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .send-btn svg { width: 22px; height: 22px; fill: white; margin-left: 3px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2)); } .settings-page { background: var(--bg-setting); min-height: 100%; padding: 24px; box-sizing: border-box; } .settings-group { background: var(--bg-card); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 24px; box-shadow: var(--shadow-sm); border: 1px solid var(--border-light); } .settings-group-title { font-size: 11px; color: var(--primary); text-transform: uppercase; padding: 20px 20px 10px; font-weight: 700; letter-spacing: 0.1em; display: flex; align-items: center; gap: 8px; } .settings-group-title::before { content: ''; width: 4px; height: 14px; background: var(--primary-gradient); border-radius: 2px; } .setting-item { padding: 18px 20px; border-bottom: 1px solid var(--border-light); transition: background var(--transition-fast); } .setting-item:last-child { border-bottom: none; } .setting-item:hover { background: var(--bg-hover); } .setting-label { font-size: 14px; font-weight: 600; color: var(--text-main); margin-bottom: 6px; display: block; } .setting-desc { font-size: 12px; color: var(--text-sec); margin-bottom: 12px; line-height: 1.6; } .setting-item-row { display: flex; justify-content: space-between; align-items: center; } .setting-item-row .setting-info { flex: 1; margin-right: 16px; } .setting-item-row .setting-label { margin-bottom: 4px; } .setting-item-row .setting-desc { margin-bottom: 0; } .toggle-switch { position: relative; width: 52px; height: 28px; flex-shrink: 0; } .toggle-switch input { opacity: 0; width: 0; height: 0; position: absolute; } .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: var(--border-medium); border-radius: var(--radius-full); transition: all var(--transition-normal); } .toggle-slider::before { content: ''; position: absolute; height: 22px; width: 22px; left: 3px; bottom: 3px; background: white; border-radius: 50%; transition: all var(--transition-normal); box-shadow: var(--shadow-sm); } .toggle-switch input:checked + .toggle-slider { background: var(--primary-gradient); box-shadow: var(--shadow-glow); } .toggle-switch input:checked + .toggle-slider::before { transform: translateX(24px); } .toggle-switch input:focus + .toggle-slider { box-shadow: 0 0 0 3px rgba(82, 166, 125, 0.2); } .spinner { width: 20px; height: 20px; border: 2.5px solid rgba(255,255,255,0.25); border-top-color: #fff; border-radius: 50%; animation: spin 0.7s linear infinite; display: none; } .btn.loading .spinner { display: inline-block; } .btn.loading .btn-text { display: none; } @keyframes spin { to { transform: rotate(360deg); } } .thinking { display: flex; gap: 5px; padding: 8px 0; } .thinking-dot { width: 8px; height: 8px; background: var(--primary); border-radius: 50%; animation: thinking 1.4s ease-in-out infinite; } .thinking-dot:nth-child(2) { animation-delay: 0.2s; } .thinking-dot:nth-child(3) { animation-delay: 0.4s; } @keyframes thinking { 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } } .tip-text { text-align: center; color: var(--text-muted); font-size: 14px; padding: 50px 24px; line-height: 2; } .tip-text strong { color: var(--primary); } .tip-text .tip-icon { font-size: 48px; display: block; margin-bottom: 16px; opacity: 0.7; } .hidden { display: none !important; } .content-area::-webkit-scrollbar { width: 6px; } .content-area::-webkit-scrollbar-track { background: rgba(82, 166, 125, 0.05); border-radius: 3px; } .content-area::-webkit-scrollbar-thumb { background: linear-gradient(180deg, var(--primary-light), var(--primary)); border-radius: 3px; } .content-area::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, var(--primary), var(--primary-dark)); } .chat-messages::-webkit-scrollbar { width: 5px; } .chat-messages::-webkit-scrollbar-track { background: transparent; } .chat-messages::-webkit-scrollbar-thumb { background: rgba(156, 139, 128, 0.25); border-radius: 3px; } .chat-messages::-webkit-scrollbar-thumb:hover { background: rgba(156, 139, 128, 0.45); } input[type="number"] { -moz-appearance: textfield; } input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .range-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; } .range-buttons { display: flex; gap: 8px; } .range-inputs { display: flex; gap: 14px; align-items: center; } .range-inputs input { flex: 1; } .range-separator { color: var(--text-muted); font-size: 18px; font-weight: 300; } .shortcut-hint { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--text-muted); margin-top: 12px; } .kbd { display: inline-flex; padding: 3px 7px; background: var(--bg-card); border: 1px solid var(--border-light); border-radius: 5px; font-family: 'JetBrains Mono', monospace; font-size: 10px; box-shadow: var(--shadow-sm); } .toast { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%) translateY(20px); background: var(--text-main); color: var(--text-inverse); padding: 12px 20px; border-radius: var(--radius-md); font-size: 12px; font-weight: 500; box-shadow: var(--shadow-lg); z-index: 10000; opacity: 0; pointer-events: none; transition: all var(--transition-normal); display: flex; align-items: center; gap: 8px; max-width: 90%; text-align: center; } .toast.show { transform: translateX(-50%) translateY(0); opacity: 1; } .toast.error { background: var(--danger); } `; const Core = { // 提取当前网页的主要内容 extractPageContent() { try { const url = window.location.href; const title = document.title || '无标题'; console.log('[AI速读] 开始提取页面内容...'); // 尝试多种方式提取正文内容 let content = ''; let extractMethod = ''; // 方法1: 查找常见的文章容器 const articleSelectors = [ 'article', '[role="main"]', 'main', '.article-content', '.post-content', '.entry-content', '.content', '#content', '.main-content' ]; let mainElement = null; for (const selector of articleSelectors) { const elements = document.querySelectorAll(selector); for (const el of elements) { const text = el.textContent || el.innerText || ''; if (text.trim().length > 100) { mainElement = el; extractMethod = `选择器: ${selector}`; break; } } if (mainElement) break; } if (mainElement) { console.log('[AI速读] 找到主内容区域:', extractMethod); // 克隆元素以避免修改原页面 const clone = mainElement.cloneNode(true); // 移除不需要的元素 const removeSelectors = [ 'script', 'style', 'nav', 'header', 'footer', '.advertisement', '.ad', '.ads', '.sidebar', '.menu', '.comments', '.comment', '.social-share', '.share', 'iframe', 'noscript', 'button', '.button', '.navigation', '.breadcrumb', '.related' ]; removeSelectors.forEach(sel => { try { clone.querySelectorAll(sel).forEach(el => el.remove()); } catch(e) {} }); // 提取文本内容,保留段落结构 const paragraphs = []; const walker = document.createTreeWalker( clone, NodeFilter.SHOW_TEXT, null, false ); let node; while (node = walker.nextNode()) { const text = node.textContent.trim(); if (text.length > 10) { paragraphs.push(text); } } content = paragraphs.join('\n'); // 清理多余的空白 content = content .replace(/\s+/g, ' ') .replace(/\n\s*\n+/g, '\n\n') .trim(); } else { console.log('[AI速读] 未找到主内容区域,使用body'); extractMethod = '使用 body'; } // 如果内容太短,使用整个body if (content.length < 100) { console.log('[AI速读] 内容太短,尝试提取body全部内容'); const bodyClone = document.body.cloneNode(true); // 移除不需要的元素 const removeSelectors = [ 'script', 'style', 'nav', 'header', 'footer', '.advertisement', '.ad', '.ads', 'iframe', 'noscript' ]; removeSelectors.forEach(sel => { try { bodyClone.querySelectorAll(sel).forEach(el => el.remove()); } catch(e) {} }); content = (bodyClone.textContent || bodyClone.innerText || '') .replace(/\s+/g, ' ') .trim(); extractMethod = '使用 body (fallback)'; } // 最终检查 if (!content || content.length < 50) { console.warn('[AI速读] 提取的内容过短:', content.length); throw new Error('页面内容过短或无法提取有效内容'); } // 限制内容长度(避免超过API限制) const maxLength = 50000; if (content.length > maxLength) { console.log(`[AI速读] 内容过长 (${content.length}字),截断到 ${maxLength} 字`); content = content.substring(0, maxLength) + '\n\n[内容过长,已截断...]'; } console.log(`[AI速读] 提取成功: ${content.length} 字 (${extractMethod})`); return { url, title, content, contentLength: content.length, extractMethod, timestamp: new Date().toLocaleString('zh-CN'), success: true }; } catch (error) { console.error('[AI速读] 提取页面内容失败:', error); return { url: window.location.href, title: document.title || '无标题', content: '', contentLength: 0, extractMethod: 'error', timestamp: new Date().toLocaleString('zh-CN'), success: false, error: error.message }; } }, parseThinkingContent(text) { if (!text) return { thinking: '', content: '' }; let thinkingParts = []; let mainContent = text; const thinkingPatterns = [ /([\s\S]*?)<\/think>/gi, /([\s\S]*?)<\/thinking>/gi, /([\s\S]*?)<\/reason>/gi, /([\s\S]*?)<\/reasoning>/gi, /([\s\S]*?)<\/reflection>/gi, /([\s\S]*?)<\/inner_thought>/gi, /([\s\S]*?)<\\think>/gi, /([\s\S]*?)<\\thinking>/gi, /<\|think\|>([\s\S]*?)<\|\/think\|>/gi, /<\|thinking\|>([\s\S]*?)<\|\/thinking\|>/gi, /\[think\]([\s\S]*?)\[\/think\]/gi, /\[thinking\]([\s\S]*?)\[\/thinking\]/gi, ]; for (const pattern of thinkingPatterns) { pattern.lastIndex = 0; let match; while ((match = pattern.exec(mainContent)) !== null) { const thinkContent = match[1].trim(); if (thinkContent) { thinkingParts.push(thinkContent); } mainContent = mainContent.replace(match[0], ''); pattern.lastIndex = 0; } } const unclosedPatterns = [ { start: //i, end: /<\/think>|<\\think>/i, tag: '' }, { start: //i, end: /<\/thinking>|<\\thinking>/i, tag: '' }, { start: /<\|think\|>/i, end: /<\|\/think\|>/i, tag: '<|think|>' }, ]; for (const { start, end, tag } of unclosedPatterns) { const startMatch = mainContent.match(start); if (startMatch && !end.test(mainContent)) { const startIdx = mainContent.indexOf(startMatch[0]); const thinkContent = mainContent.slice(startIdx + startMatch[0].length).trim(); if (thinkContent) { thinkingParts.push(thinkContent + ' ⏳'); mainContent = mainContent.slice(0, startIdx); } break; } } return { thinking: thinkingParts.join('\n\n'), content: mainContent.trim() }; }, renderWithThinking(text, isStreaming = false, keepExpanded = false) { const { thinking, content } = this.parseThinkingContent(text); const arrowIcon = ``; let html = ''; if (thinking) { const charCount = thinking.length; const streamingClass = isStreaming ? ' streaming' : ''; const expandedClass = keepExpanded ? ' expanded' : ''; const statusText = isStreaming ? '思考中...' : `${charCount} 字`; const lines = thinking.split('\n').filter(l => l.trim()); const previewLines = lines.slice(-4).join('\n'); const previewText = previewLines.length > 150 ? '...' + previewLines.slice(-150) : previewLines; const thinkingHtml = DOMPurify.sanitize(marked.parse(thinking)); const previewHtml = DOMPurify.sanitize(marked.parse(previewText)); html += `
💡
思考过程 ${statusText}
${arrowIcon}
${previewHtml}
${thinkingHtml}
`; } if (content) { html += DOMPurify.sanitize(marked.parse(content)); } return html; }, escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }, async streamChat(messages, onChunk, onDone, onError) { const key = GM_getValue('apiKey', ''); const url = GM_getValue('apiUrl', 'https://api.openai.com/v1/chat/completions'); const model = GM_getValue('model', 'deepseek-chat'); const useStream = GM_getValue('useStream', true); if (!key) return onError("未配置 API Key,请先在设置中配置"); try { const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}` }, body: JSON.stringify({ model, messages, stream: useStream }) }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); if (useStream) { const reader = resp.body.getReader(); const decoder = new TextDecoder(); let reasoningBuffer = ''; let contentStarted = false; let thinkTagSent = false; while (true) { const { done, value } = await reader.read(); if (done) break; const lines = decoder.decode(value, { stream: true }).split('\n'); for (const line of lines) { if (line.startsWith('data: ') && line !== 'data: [DONE]') { try { const json = JSON.parse(line.slice(6)); const delta = json.choices?.[0]?.delta; if (delta?.reasoning_content) { if (!thinkTagSent) { onChunk(''); thinkTagSent = true; } onChunk(delta.reasoning_content); reasoningBuffer += delta.reasoning_content; } if (delta?.content) { if (thinkTagSent && !contentStarted) { onChunk(''); contentStarted = true; } onChunk(delta.content); } } catch(e){} } } } if (thinkTagSent && !contentStarted) { onChunk('
'); } } else { const data = await resp.json(); const message = data.choices?.[0]?.message; let fullContent = ''; if (message?.reasoning_content) { fullContent += `${message.reasoning_content}`; } if (message?.content) { fullContent += message.content; } if (fullContent) onChunk(fullContent); } onDone(); } catch (e) { onError(e.message); } } }; class AppUI { constructor() { this.host = document.createElement('div'); this.host.id = 'ld-summary-pro'; document.body.appendChild(this.host); this.shadow = this.host.attachShadow({ mode: 'open' }); this.isOpen = false; this.btnPos = GM_getValue('btnPos', { side: 'right', top: '50%' }); this.side = this.btnPos.side; this.sidebarWidth = GM_getValue('sidebarWidth', 420); this.isDarkTheme = GM_getValue('isDarkTheme', false); this.chatHistory = []; this.postContent = ''; this.lastSummary = ''; this.isGenerating = false; this.currentTab = 'summary'; this.userMessageCount = 0; this.userScrolledUp = false; this.isProgrammaticScroll = false; this.init(); } init() { const style = document.createElement('style'); style.textContent = STYLES; this.shadow.appendChild(style); this.render(); this.restoreState(); // 使用 setTimeout 确保 DOM 完全渲染后再绑定事件 setTimeout(() => { this.bindEvents(); this.bindKeyboardShortcuts(); console.log('[AI速读] 初始化完成'); }, 0); } Q(s) { return this.shadow.querySelector(s); } render() { const arrowLeft = ``; const arrowRight = ``; const sendIcon = ``; const arrowUpIcon = ``; const arrowDownIcon = ``; // 创建容器元素而不是使用 innerHTML += const container = document.createElement('div'); container.innerHTML = `
${arrowLeft}
`; // 将容器添加到 shadow DOM this.shadow.appendChild(container); } restoreState() { this.host.style.setProperty('--sidebar-width', `${this.sidebarWidth}px`); const btn = this.Q('#toggle-btn'); btn.style.top = this.btnPos.top; this.applySideState(); if (this.isDarkTheme) { this.host.classList.add('dark-theme'); this.Q('#btn-theme').textContent = '☀️'; } this.Q('#cfg-url').value = GM_getValue('apiUrl', 'https://api.deepseek.com/v1/chat/completions'); this.Q('#cfg-key').value = GM_getValue('apiKey', ''); this.Q('#cfg-model').value = GM_getValue('model', 'deepseek-chat'); this.Q('#cfg-prompt-sum').value = GM_getValue('prompt_sum', '请总结以下网页内容。使用 Markdown 格式,条理清晰,重点突出主要观点、关键信息和核心内容。适当使用标题、列表和引用来组织内容。'); this.Q('#cfg-prompt-chat').value = GM_getValue('prompt_chat', '你是一个网页内容阅读助手。基于上文中的网页内容,回答用户的问题。回答要准确、简洁,必要时引用原文。'); this.Q('#cfg-stream').checked = GM_getValue('useStream', true); this.Q('#cfg-autoscroll').checked = GM_getValue('autoScroll', true); // 显示当前页面信息 this.updatePageInfo(); } applySideState() { const btn = this.Q('#toggle-btn'); const sidebar = this.Q('#sidebar'); const resizer = this.Q('#resizer'); const arrowLeft = ``; const arrowRight = ``; btn.style.left = ''; btn.style.right = ''; if (this.side === 'left') { sidebar.className = 'sidebar-panel panel-left' + (this.isOpen ? ' open' : ''); resizer.className = 'resize-handle handle-left'; btn.className = 'btn-snap-left' + (this.isOpen ? ' arrow-flip' : ''); btn.innerHTML = arrowRight; } else { sidebar.className = 'sidebar-panel panel-right' + (this.isOpen ? ' open' : ''); resizer.className = 'resize-handle handle-right'; btn.className = 'btn-snap-right' + (this.isOpen ? ' arrow-flip' : ''); btn.innerHTML = arrowLeft; } this.updateButtonPosition(); } updateButtonPosition(useTransition = true) { const btn = this.Q('#toggle-btn'); if (!useTransition) { btn.style.transition = 'none'; } else { btn.style.transition = ''; } if (this.side === 'left') { btn.style.right = 'auto'; btn.style.left = this.isOpen ? `${this.sidebarWidth}px` : '0'; } else { btn.style.left = 'auto'; btn.style.right = this.isOpen ? `${this.sidebarWidth}px` : '0'; } if (!useTransition) { btn.offsetHeight; requestAnimationFrame(() => { btn.style.transition = ''; }); } } bindEvents() { const btn = this.Q('#toggle-btn'); // 检查关键元素是否存在 const saveBtn = this.Q('#btn-save'); console.log('[AI速读] 检查元素:', { toggleBtn: !!btn, saveBtn: !!saveBtn, cfgUrl: !!this.Q('#cfg-url'), cfgKey: !!this.Q('#cfg-key'), cfgModel: !!this.Q('#cfg-model') }); this.shadow.addEventListener('click', (e) => { const toggle = e.target.closest('[data-thinking-toggle]'); if (toggle) { const block = toggle.closest('[data-thinking-block]'); if (block) { block.classList.toggle('expanded'); } } }); let isDrag = false, hasMoved = false, startX, startY, startRect; btn.addEventListener('mousedown', (e) => { isDrag = true; hasMoved = false; startX = e.clientX; startY = e.clientY; startRect = btn.getBoundingClientRect(); if (!this.isOpen) { btn.style.transition = 'none'; } btn.style.cursor = 'grabbing'; e.preventDefault(); }); window.addEventListener('mousemove', (e) => { if (!isDrag) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (Math.abs(dx) > 5 || Math.abs(dy) > 5) hasMoved = true; if (!this.isOpen && hasMoved) { btn.style.left = `${startRect.left + dx}px`; btn.style.top = `${startRect.top + dy}px`; btn.style.right = 'auto'; btn.className = 'btn-floating'; } }); window.addEventListener('mouseup', (e) => { if (!isDrag) return; isDrag = false; btn.style.cursor = 'grab'; btn.style.transition = ''; if (hasMoved && !this.isOpen) { const winW = window.innerWidth; const btnRect = btn.getBoundingClientRect(); const centerX = btnRect.left + btnRect.width / 2; this.side = centerX < winW / 2 ? 'left' : 'right'; let newTop = btnRect.top; if (newTop < 10) newTop = 10; if (newTop > window.innerHeight - 60) newTop = window.innerHeight - 60; this.btnPos = { side: this.side, top: `${newTop}px` }; GM_setValue('btnPos', this.btnPos); btn.style.top = `${newTop}px`; this.applySideState(); } else if (!hasMoved) { this.toggleSidebar(); } }); this.Q('#btn-close').onclick = () => this.toggleSidebar(); this.Q('#btn-theme').onclick = () => this.toggleTheme(); this.shadow.querySelectorAll('.tab-item').forEach(tab => { tab.onclick = () => { const tabName = tab.dataset.tab; this.switchTab(tabName); }; }); let isResizing = false; this.Q('#resizer').addEventListener('mousedown', (e) => { isResizing = true; document.body.style.cursor = 'col-resize'; this.Q('#sidebar').style.transition = 'none'; document.body.style.transition = 'none'; e.preventDefault(); }); window.addEventListener('mousemove', (e) => { if (!isResizing) return; let newW = this.side === 'right' ? (window.innerWidth - e.clientX) : e.clientX; if (newW > 320 && newW < 700) { this.sidebarWidth = newW; this.host.style.setProperty('--sidebar-width', `${newW}px`); if (this.isOpen) { this.squeezeBody(true); this.updateButtonPosition(false); } } }); window.addEventListener('mouseup', () => { if (isResizing) { isResizing = false; document.body.style.cursor = ''; this.Q('#sidebar').style.transition = ''; document.body.style.transition = 'margin 0.35s cubic-bezier(0.4, 0, 0.2, 1)'; GM_setValue('sidebarWidth', this.sidebarWidth); } }); // 移除了 range-all 和 range-recent 按钮的绑定(通用版本不需要) this.Q('#btn-summary').onclick = () => this.doSummary(); this.Q('#btn-send').onclick = () => this.doChat(); this.Q('#chat-input').onkeydown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.doChat(); } }; this.Q('#chat-input').addEventListener('input', (e) => { const el = e.target; el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 140) + 'px'; }); this.Q('#btn-clear-chat').onclick = () => this.clearChat(); this.Q('#btn-scroll-top').onclick = () => this.scrollToTop(); this.Q('#btn-scroll-bottom').onclick = () => this.forceScrollToBottom(); const chatMessages = this.Q('#chat-messages'); let lastScrollTop = 0; chatMessages.addEventListener('scroll', () => { const currentScrollTop = chatMessages.scrollTop; const scrollHeight = chatMessages.scrollHeight; const clientHeight = chatMessages.clientHeight; const isNearBottom = scrollHeight - currentScrollTop - clientHeight < 80; if (this.isGenerating && !this.isProgrammaticScroll) { if (currentScrollTop < lastScrollTop - 10) { this.userScrolledUp = true; } else if (isNearBottom) { this.userScrolledUp = false; } } lastScrollTop = currentScrollTop; this.updateScrollButtons(); }); // 保存按钮事件绑定 this.Q('#btn-save').onclick = () => { console.log('[AI速读] 保存设置...'); try { GM_setValue('apiUrl', this.Q('#cfg-url').value.trim()); GM_setValue('apiKey', this.Q('#cfg-key').value.trim()); GM_setValue('model', this.Q('#cfg-model').value.trim()); GM_setValue('prompt_sum', this.Q('#cfg-prompt-sum').value); GM_setValue('prompt_chat', this.Q('#cfg-prompt-chat').value); GM_setValue('useStream', this.Q('#cfg-stream').checked); GM_setValue('autoScroll', this.Q('#cfg-autoscroll').checked); console.log('[AI速读] 设置保存成功'); this.showToast('设置已保存', 'success'); this.switchTab('summary'); } catch (error) { console.error('[AI速读] 保存设置失败:', error); this.showToast('保存失败: ' + error.message, 'error'); } }; console.log('[AI速读] 事件绑定完成'); } bindKeyboardShortcuts() { document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 's') { e.preventDefault(); this.toggleSidebar(); } if (e.key === 'Escape' && this.isOpen) { this.toggleSidebar(); } }); } toggleTheme() { this.isDarkTheme = !this.isDarkTheme; GM_setValue('isDarkTheme', this.isDarkTheme); if (this.isDarkTheme) { this.host.classList.add('dark-theme'); this.Q('#btn-theme').textContent = '☀️'; } else { this.host.classList.remove('dark-theme'); this.Q('#btn-theme').textContent = '🌙'; } } showToast(message, type = '') { const toast = this.Q('#toast'); toast.textContent = message; toast.className = 'toast' + (type ? ` ${type}` : ''); requestAnimationFrame(() => { toast.classList.add('show'); }); setTimeout(() => { toast.classList.remove('show'); }, 2500); } copyToClipboard(text) { try { GM_setClipboard(text, 'text'); return true; } catch (e) { const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); return true; } } updateScrollButtons() { const chatMessages = this.Q('#chat-messages'); const scrollTop = chatMessages.scrollTop; const scrollHeight = chatMessages.scrollHeight; const clientHeight = chatMessages.clientHeight; const distanceToBottom = scrollHeight - scrollTop - clientHeight; const btnTop = this.Q('#btn-scroll-top'); const btnBottom = this.Q('#btn-scroll-bottom'); if (scrollTop > 50) { btnTop.classList.add('visible'); } else { btnTop.classList.remove('visible'); } if (this.isGenerating && this.userScrolledUp) { btnBottom.classList.add('visible', 'generating'); } else if (distanceToBottom > 50) { btnBottom.classList.add('visible'); btnBottom.classList.remove('generating'); } else { btnBottom.classList.remove('visible', 'generating'); } } scrollToTop() { const chatMessages = this.Q('#chat-messages'); chatMessages.scrollTo({ top: 0, behavior: 'smooth' }); } scrollToBottom(force = false) { if (!force && !GM_getValue('autoScroll', true)) { this.updateScrollButtons(); return; } if (!force && this.userScrolledUp) { this.updateScrollButtons(); return; } const chatMessages = this.Q('#chat-messages'); this.isProgrammaticScroll = true; setTimeout(() => { chatMessages.scrollTop = chatMessages.scrollHeight; setTimeout(() => { this.isProgrammaticScroll = false; this.updateScrollButtons(); }, 50); }, 0); } forceScrollToBottom() { this.userScrolledUp = false; const chatMessages = this.Q('#chat-messages'); this.isProgrammaticScroll = true; setTimeout(() => { chatMessages.scrollTop = chatMessages.scrollHeight; setTimeout(() => { this.isProgrammaticScroll = false; this.updateScrollButtons(); }, 50); }, 0); } clearChat() { if (this.chatHistory.length === 0) return; if (confirm('确定要清空所有对话记录吗?\n(总结上下文将保留,可以继续提问)')) { if (this.chatHistory.length > 3) { this.chatHistory = this.chatHistory.slice(0, 3); } this.Q('#chat-list').innerHTML = ''; this.userMessageCount = 0; this.updateMessageCount(); if (this.chatHistory.length <= 3) { const emptyDiv = this.Q('#chat-empty'); emptyDiv.classList.remove('hidden'); emptyDiv.innerHTML = '💬对话已清空
可以继续基于网页内容提问'; } this.showToast('对话已清空'); } } updateMessageCount() { this.Q('#msg-count').textContent = this.userMessageCount; } toggleSidebar() { this.isOpen = !this.isOpen; const sidebar = this.Q('#sidebar'); const btn = this.Q('#toggle-btn'); if (this.isOpen) { sidebar.classList.add('open'); btn.classList.add('arrow-flip'); this.squeezeBody(true); this.updatePageInfo(); } else { sidebar.classList.remove('open'); btn.classList.remove('arrow-flip'); this.squeezeBody(false); } this.updateButtonPosition(); } squeezeBody(active) { const body = document.body; body.style.transition = 'margin 0.35s cubic-bezier(0.4, 0, 0.2, 1)'; if (!active) { body.style.marginLeft = ''; body.style.marginRight = ''; } else { if (this.side === 'left') { body.style.marginLeft = `${this.sidebarWidth}px`; body.style.marginRight = ''; } else { body.style.marginRight = `${this.sidebarWidth}px`; body.style.marginLeft = ''; } } } switchTab(tabName) { this.shadow.querySelectorAll('.tab-item').forEach(t => { t.classList.toggle('active', t.dataset.tab === tabName); }); this.shadow.querySelectorAll('.view-page').forEach(p => { p.classList.toggle('active', p.id === `page-${tabName}`); }); this.currentTab = tabName; if (tabName === 'chat') { setTimeout(() => this.updateScrollButtons(), 100); } if (tabName === 'summary') { this.updatePageInfo(); } } updatePageInfo() { try { const pageData = Core.extractPageContent(); const titleEl = this.Q('#page-title-display'); const urlEl = this.Q('#page-url-display'); if (titleEl) titleEl.textContent = pageData.title || '无标题'; if (urlEl) { urlEl.textContent = pageData.url; urlEl.title = pageData.url; } // 在控制台输出调试信息 if (pageData.success) { console.log('[AI速读] 页面信息更新成功:', { title: pageData.title, contentLength: pageData.contentLength, method: pageData.extractMethod }); } else { console.error('[AI速读] 页面信息更新失败:', pageData.error); } } catch (error) { console.error('[AI速读] updatePageInfo 错误:', error); } } initRangeInputs() { // 通用版本不需要楼层范围 } setRange(type) { // 通用版本不需要楼层范围 } setLoading(btnId, isLoading) { const btn = this.Q(btnId); this.isGenerating = isLoading; btn.disabled = isLoading; btn.classList.toggle('loading', isLoading); if (btnId === '#btn-send') { const input = this.Q('#chat-input'); if (input) { input.disabled = isLoading; input.placeholder = isLoading ? '正在生成回复...' : '输入你的问题... (Enter 发送)'; } } } async doSummary() { this.setLoading('#btn-summary', true); const resultBox = this.Q('#summary-result'); resultBox.classList.remove('empty'); resultBox.innerHTML = `
正在提取网页内容...
`; try { // 提取当前页面内容 const pageData = Core.extractPageContent(); console.log('[AI速读] 页面数据:', pageData); if (!pageData.success || !pageData.content || pageData.content.length < 50) { const errorMsg = pageData.error || '未能提取到有效的网页内容'; throw new Error(`${errorMsg}\n\n提示:\n1. 请确保页面已完全加载\n2. 某些动态加载的页面可能需要等待内容加载完成\n3. 可以尝试刷新页面后重试\n\n提取到的内容长度: ${pageData.contentLength || 0} 字`); } this.postContent = `标题: ${pageData.title}\nURL: ${pageData.url}\n时间: ${pageData.timestamp}\n提取方式: ${pageData.extractMethod}\n内容长度: ${pageData.contentLength} 字\n\n内容:\n${pageData.content}`; resultBox.innerHTML = `
AI 正在分析中... (已提取 ${pageData.contentLength} 字)
`; const sysPrompt = GM_getValue('prompt_sum', ''); const messages = [ { role: 'system', content: sysPrompt }, { role: 'user', content: `网页内容:\n${this.postContent}` } ]; let aiText = ''; await Core.streamChat(messages, (chunk) => { aiText += chunk; const currentBlock = resultBox.querySelector('[data-thinking-block]'); const isExpanded = currentBlock?.classList.contains('expanded') || false; resultBox.innerHTML = `
` + Core.renderWithThinking(aiText, true, isExpanded); if (GM_getValue('autoScroll', true)) { setTimeout(() => { resultBox.scrollTop = resultBox.scrollHeight; const thinkingInner = resultBox.querySelector('.thinking-content-inner'); if (thinkingInner && isExpanded) { thinkingInner.scrollTop = thinkingInner.scrollHeight; } }, 0); } const copyBtn = this.Q('#btn-copy-summary'); if (copyBtn) { copyBtn.onclick = () => { this.copyToClipboard(aiText); copyBtn.classList.add('copied'); copyBtn.textContent = '✓ 已复制'; setTimeout(() => { copyBtn.classList.remove('copied'); copyBtn.textContent = '📋 复制'; }, 2000); }; } }, () => { this.setLoading('#btn-summary', false); resultBox.innerHTML = `
` + Core.renderWithThinking(aiText, false); const copyBtn = this.Q('#btn-copy-summary'); if (copyBtn) { copyBtn.onclick = () => { this.copyToClipboard(aiText); copyBtn.classList.add('copied'); copyBtn.textContent = '✓ 已复制'; setTimeout(() => { copyBtn.classList.remove('copied'); copyBtn.textContent = '📋 复制'; }, 2000); }; } this.lastSummary = aiText; const chatPrompt = GM_getValue('prompt_chat', ''); this.chatHistory = [ { role: 'system', content: chatPrompt }, { role: 'user', content: `以下是网页内容供你参考:\n${this.postContent}` }, { role: 'assistant', content: aiText } ]; this.Q('#chat-list').innerHTML = ''; this.userMessageCount = 0; this.updateMessageCount(); this.Q('#chat-empty').classList.remove('hidden'); this.Q('#chat-empty').innerHTML = '总结已完成!
现在可以基于网页内容进行对话'; console.log('[AI速读] 总结完成'); }, (err) => { resultBox.innerHTML = `
❌ 错误: ${err}
`; this.setLoading('#btn-summary', false); this.showToast('总结失败: ' + err, 'error'); console.error('[AI速读] 总结失败:', err); } ); } catch(e) { const errorHtml = `
❌ 错误: ${Core.escapeHtml(e.message.split('\n')[0])}
${e.message.includes('提示:') ? `
${Core.escapeHtml(e.message.split('提示:')[1])}
` : ''}
`; resultBox.innerHTML = errorHtml; this.setLoading('#btn-summary', false); console.error('[AI速读] doSummary 错误:', e); } } async doChat() { if (this.isGenerating) return; if (this.chatHistory.length === 0) { return alert('请先在「总结」页面生成网页内容摘要'); } const input = this.Q('#chat-input'); const txt = input.value.trim(); if (!txt) return; input.value = ''; input.style.height = 'auto'; this.Q('#chat-empty').classList.add('hidden'); this.userScrolledUp = false; this.addBubble('user', txt); this.chatHistory.push({ role: 'user', content: txt }); this.userMessageCount++; this.updateMessageCount(); const msgDiv = this.addBubble('ai', ''); msgDiv.innerHTML = `
`; let aiText = ''; this.setLoading('#btn-send', true); await Core.streamChat(this.chatHistory, (chunk) => { aiText += chunk; const currentBlock = msgDiv.querySelector('[data-thinking-block]'); const isExpanded = currentBlock?.classList.contains('expanded') || false; msgDiv.innerHTML = Core.renderWithThinking(aiText, true, isExpanded); if (GM_getValue('autoScroll', true) && isExpanded) { setTimeout(() => { const thinkingInner = msgDiv.querySelector('.thinking-content-inner'); if (thinkingInner) { thinkingInner.scrollTop = thinkingInner.scrollHeight; } }, 0); } this.scrollToBottom(); }, () => { msgDiv.innerHTML = Core.renderWithThinking(aiText, false); this.chatHistory.push({ role: 'assistant', content: aiText }); this.setLoading('#btn-send', false); this.userScrolledUp = false; this.updateScrollButtons(); }, (err) => { msgDiv.innerHTML += `
❌ ${err}`; this.setLoading('#btn-send', false); } ); } addBubble(role, text) { const div = document.createElement('div'); div.className = `bubble bubble-${role}`; div.innerHTML = role === 'user' ? text : Core.renderWithThinking(text); this.Q('#chat-list').appendChild(div); this.scrollToBottom(); return div; } } window.addEventListener('load', () => new AppUI()); })();