A browser extension that lets you summarize any webpage and ask questions using AI.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

follow up questions + remove detailed summary

+533 -183
+255 -37
popup/popup.css
··· 355 355 /* ── Footer ── */ 356 356 .footer { 357 357 display: flex; 358 - gap: 8px; 359 358 padding: 11px 18px; 360 359 border-top: 1px solid var(--border); 361 360 flex-shrink: 0; 362 361 align-items: center; 363 - } 364 - 365 - .footer-buttons { 366 - display: flex; 367 - gap: 8px; 368 - flex: 1; 369 362 } 370 363 371 364 .footer-btn { ··· 380 373 cursor: pointer; 381 374 transition: all 0.1s ease; 382 375 border: none; 383 - } 384 - 385 - .footer-btn.primary { 386 376 flex: 1; 387 377 background: var(--primary-bg); 388 378 color: var(--primary-text); 389 379 } 390 380 391 - .footer-btn.primary:hover { background: var(--primary-bg-hover); } 392 - .footer-btn.primary:active { background: var(--primary-bg-active); } 393 - 394 - .footer-btn.secondary { 395 - background: transparent; 396 - color: var(--secondary-color); 397 - border: 1px solid var(--border); 398 - } 399 - 400 - .footer-btn.secondary:hover { 401 - color: var(--icon-btn-hover); 402 - border-color: var(--border-hover); 403 - } 404 - 381 + .footer-btn:hover { background: var(--primary-bg-hover); } 382 + .footer-btn:active { background: var(--primary-bg-active); } 405 383 .footer-btn:disabled { 406 384 opacity: 0.35; 407 385 cursor: not-allowed; 408 386 } 409 387 410 - /* Copy / icon button in footer */ 411 - .footer-icon-btn { 412 - width: 34px; 413 - height: 34px; 414 - border-radius: 6px; 415 - border: 1px solid var(--border); 416 - flex-shrink: 0; 417 - } 418 - 419 - .footer-icon-btn:hover { 420 - border-color: var(--border-hover); 421 - } 422 - 423 388 .hidden { 424 389 display: none !important; 425 390 } ··· 455 420 from { opacity: 0; transform: translateX(-50%) translateY(4px); } 456 421 to { opacity: 1; transform: translateX(-50%) translateY(0); } 457 422 } 423 + 424 + /* ── Chat ── */ 425 + .chat-container { 426 + padding: 12px 18px; 427 + border-top: 1px solid var(--border); 428 + flex-shrink: 0; 429 + } 430 + 431 + .chat-container.hidden { 432 + display: none; 433 + } 434 + 435 + /* Chat messages are rendered inside .result, sharing the same scroll */ 436 + .chat-divider { 437 + margin: 20px 0 16px 0; 438 + border: none; 439 + border-top: 1px solid var(--border); 440 + } 441 + 442 + .chat-message { 443 + animation: fadeUp 0.2s ease; 444 + } 445 + 446 + .chat-message + .chat-message { 447 + margin-top: 14px; 448 + } 449 + 450 + /* User messages as bubbles */ 451 + .chat-message.user { 452 + background: var(--primary-bg); 453 + color: var(--primary-text); 454 + padding: 10px 14px; 455 + border-radius: 12px; 456 + border-bottom-right-radius: 4px; 457 + font-size: 13px; 458 + line-height: 1.5; 459 + max-width: 85%; 460 + margin-left: auto; 461 + } 462 + 463 + /* Assistant messages styled like summary content */ 464 + .chat-message.assistant { 465 + font-size: 14px; 466 + line-height: 1.7; 467 + color: var(--text-secondary); 468 + } 469 + 470 + .chat-message.assistant.loading { 471 + display: flex; 472 + align-items: center; 473 + justify-content: center; 474 + padding: 20px 0; 475 + } 476 + 477 + .chat-message.assistant.error { 478 + background: var(--error-bg); 479 + border: 1px solid var(--error-border); 480 + color: var(--error-text); 481 + padding: 10px 14px; 482 + border-radius: 6px; 483 + } 484 + 485 + .chat-spinner { 486 + width: 18px; 487 + height: 18px; 488 + border: 1.5px solid var(--spinner-track); 489 + border-top-color: var(--spinner-head); 490 + border-radius: 50%; 491 + animation: spin 0.75s linear infinite; 492 + } 493 + 494 + /* Chat message markdown (assistant uses same styles as .result) */ 495 + .chat-message.assistant > * + * { 496 + margin-top: 10px; 497 + } 498 + 499 + .chat-message.assistant h1, 500 + .chat-message.assistant h2, 501 + .chat-message.assistant h3, 502 + .chat-message.assistant h4, 503 + .chat-message.assistant h5, 504 + .chat-message.assistant h6 { 505 + font-weight: 600; 506 + color: var(--heading); 507 + line-height: 1.3; 508 + } 509 + 510 + .chat-message.assistant h1 { font-size: 16px; margin-top: 16px; } 511 + .chat-message.assistant h2 { font-size: 14px; margin-top: 14px; } 512 + .chat-message.assistant h3 { font-size: 13px; margin-top: 12px; } 513 + 514 + .chat-message.assistant p { 515 + line-height: 1.65; 516 + } 517 + 518 + .chat-message.assistant p + p { 519 + margin-top: 6px; 520 + } 521 + 522 + .chat-message.assistant ul, 523 + .chat-message.assistant ol { 524 + padding-left: 18px; 525 + } 526 + 527 + .chat-message.assistant li { 528 + line-height: 1.6; 529 + } 530 + 531 + .chat-message.assistant li + li { 532 + margin-top: 2px; 533 + } 534 + 535 + .chat-message.assistant strong { 536 + font-weight: 600; 537 + color: var(--strong); 538 + } 539 + 540 + .chat-message.assistant code { 541 + font-family: 'SF Mono', 'Cascadia Code', monospace; 542 + font-size: 11.5px; 543 + background: var(--code-bg); 544 + padding: 1px 5px; 545 + border-radius: 3px; 546 + color: var(--code-text); 547 + } 548 + 549 + .chat-message.assistant pre { 550 + background: var(--code-bg); 551 + padding: 10px 12px; 552 + border-radius: 5px; 553 + overflow-x: auto; 554 + } 555 + 556 + .chat-message.assistant pre code { 557 + background: transparent; 558 + padding: 0; 559 + color: var(--pre-text); 560 + } 561 + 562 + .chat-message.assistant blockquote { 563 + border-left: 2px solid var(--border); 564 + padding: 2px 12px; 565 + color: var(--blockquote); 566 + font-style: italic; 567 + } 568 + 569 + .chat-message.assistant a { 570 + color: var(--link); 571 + text-decoration: underline; 572 + } 573 + 574 + .chat-message.assistant a:hover { 575 + color: var(--link-hover); 576 + } 577 + 578 + /* User message markdown */ 579 + .chat-message.user p { 580 + margin: 0; 581 + } 582 + 583 + .chat-message.user p + p { 584 + margin-top: 4px; 585 + } 586 + 587 + .chat-message.user strong { 588 + font-weight: 600; 589 + } 590 + 591 + .chat-input-wrap { 592 + display: flex; 593 + gap: 8px; 594 + align-items: center; 595 + } 596 + 597 + .chat-input { 598 + flex: 1; 599 + padding: 8px 12px; 600 + border: 1px solid var(--border); 601 + border-radius: 6px; 602 + background: var(--bg); 603 + color: var(--text); 604 + font-family: inherit; 605 + font-size: 13px; 606 + outline: none; 607 + transition: border-color 0.1s; 608 + } 609 + 610 + .chat-input:focus { 611 + border-color: var(--border-hover); 612 + } 613 + 614 + .chat-input::placeholder { 615 + color: var(--text-muted); 616 + } 617 + 618 + .chat-send-btn { 619 + width: 32px; 620 + height: 32px; 621 + border-radius: 6px; 622 + border: 1px solid var(--border); 623 + background: var(--bg); 624 + color: var(--icon-btn); 625 + cursor: pointer; 626 + display: flex; 627 + align-items: center; 628 + justify-content: center; 629 + transition: all 0.1s; 630 + flex-shrink: 0; 631 + } 632 + 633 + .chat-send-btn:hover { 634 + border-color: var(--border-hover); 635 + color: var(--icon-btn-hover); 636 + } 637 + 638 + .chat-send-btn:disabled { 639 + opacity: 0.35; 640 + cursor: not-allowed; 641 + } 642 + 643 + /* Summary action buttons container */ 644 + .summary-actions { 645 + display: flex; 646 + gap: 8px; 647 + margin-top: 16px; 648 + } 649 + 650 + /* Copy/regenerate button under summary */ 651 + .copy-summary-btn { 652 + display: inline-flex; 653 + align-items: center; 654 + gap: 6px; 655 + padding: 6px 12px; 656 + border-radius: 6px; 657 + border: 1px solid var(--border); 658 + background: transparent; 659 + color: var(--text-muted); 660 + font-family: inherit; 661 + font-size: 12px; 662 + font-weight: 500; 663 + cursor: pointer; 664 + transition: all 0.1s; 665 + } 666 + 667 + .copy-summary-btn:hover { 668 + border-color: var(--border-hover); 669 + color: var(--text); 670 + } 671 + 672 + .copy-summary-btn.copied { 673 + color: var(--text); 674 + border-color: var(--border-hover); 675 + }
+13 -18
popup/popup.html
··· 55 55 </div> 56 56 <div id="result" class="result hidden"></div> 57 57 </div> 58 - 59 - <div class="footer"> 60 - <div class="footer-buttons"> 61 - <button id="summarize-btn" class="footer-btn primary"> 62 - <span id="summarize-label">Quick Summary</span> 63 - </button> 64 - <button id="detail-btn" class="footer-btn secondary hidden"> 65 - <span>More Detail</span> 58 + <div id="chat-container" class="chat-container hidden"> 59 + <div class="chat-input-wrap"> 60 + <input type="text" id="chat-input" class="chat-input" placeholder="Ask about this article..." /> 61 + <button id="chat-send" class="chat-send-btn" title="Send"> 62 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 63 + <line x1="22" y1="2" x2="11" y2="13"></line> 64 + <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon> 65 + </svg> 66 66 </button> 67 67 </div> 68 - <!-- Copy icon button --> 69 - <button id="copy-btn" class="icon-btn footer-icon-btn hidden" title="Copy summary"> 70 - <svg id="copy-icon-default" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 71 - <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/> 72 - <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/> 73 - </svg> 74 - <svg id="copy-icon-done" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="hidden"> 75 - <polyline points="20 6 9 17 4 12"/> 76 - </svg> 68 + </div> 69 + 70 + <div class="footer" id="footer"> 71 + <button id="summarize-btn" class="footer-btn primary"> 72 + <span id="summarize-label">Quick Summary</span> 77 73 </button> 78 - 79 74 </div> 80 75 </div> 81 76
+262 -127
popup/popup.js
··· 7 7 let currentSummaryMode = "none"; // "none", "quick", "detailed" 8 8 let currentTabId = null; 9 9 let currentTabUrl = ""; 10 + let chatHistory = []; // Array of {role: "user" | "assistant", content: string} 11 + let isChatLoading = false; 10 12 11 13 const resultContainer = document.getElementById("result"); 12 14 const initialState = document.getElementById("initial-state"); 13 15 const summarizeBtn = document.getElementById("summarize-btn"); 14 16 const summarizeLabel = document.getElementById("summarize-label"); 15 - const detailBtn = document.getElementById("detail-btn"); 16 - const copyBtn = document.getElementById("copy-btn"); 17 17 const settingsBtn = document.getElementById("settings-btn"); 18 18 const themeBtn = document.getElementById("theme-btn"); 19 + const chatContainer = document.getElementById("chat-container"); 20 + const chatInput = document.getElementById("chat-input"); 21 + const chatSendBtn = document.getElementById("chat-send"); 22 + const footer = document.getElementById("footer"); 19 23 20 24 // Cache key prefix for session storage 21 25 const QUICK_SUMMARY_CACHE_PREFIX = "quick_summary_cache_"; 22 26 const DETAILED_SUMMARY_CACHE_PREFIX = "detailed_summary_cache_"; 23 27 const CONTENT_CACHE_PREFIX = "content_cache_"; 28 + const CHAT_CACHE_PREFIX = "chat_cache_"; 24 29 25 30 // ── Theme logic ────────────────────────────────────────────── 26 31 // Cycles: light → dark → system ··· 78 83 const cached = await chrome.storage.session.get([ 79 84 QUICK_SUMMARY_CACHE_PREFIX + currentTabId, 80 85 DETAILED_SUMMARY_CACHE_PREFIX + currentTabId, 81 - CONTENT_CACHE_PREFIX + currentTabId 86 + CONTENT_CACHE_PREFIX + currentTabId, 87 + CHAT_CACHE_PREFIX + currentTabId 82 88 ]); 83 89 84 90 const cachedQuickSummary = cached[QUICK_SUMMARY_CACHE_PREFIX + currentTabId]; 85 91 const cachedDetailedSummary = cached[DETAILED_SUMMARY_CACHE_PREFIX + currentTabId]; 86 92 const cachedContent = cached[CONTENT_CACHE_PREFIX + currentTabId]; 93 + const cachedChat = cached[CHAT_CACHE_PREFIX + currentTabId]; 87 94 88 95 resetUI(); 89 96 summarizeBtn.disabled = true; ··· 102 109 if (cachedDetailedSummary) { 103 110 detailedSummary = cachedDetailedSummary.summary; 104 111 } 112 + // Restore chat history if it exists 113 + if (cachedChat && cachedChat.messages) { 114 + chatHistory = cachedChat.messages; 115 + } 105 116 106 117 // Build combined display if we have any summaries 107 118 if (quickSummary || detailedSummary) { ··· 115 126 } 116 127 showSummary(combinedContent); 117 128 setSummarizeLabel("Regenerate"); 118 - // Show detail button only if we have quick but not detailed 119 - if (quickSummary && !detailedSummary) { 120 - detailBtn.classList.remove("hidden"); 121 - } else { 122 - detailBtn.classList.add("hidden"); 123 - } 129 + // Show chat if we have a summary and chat history 130 + showChat(); 131 + renderChatMessages(); 124 132 } else { 125 133 // No summaries yet 126 134 setSummarizeLabel("Quick Summary"); 127 - detailBtn.classList.add("hidden"); 128 135 } 129 136 } else { 130 137 // No cache, extract fresh content ··· 132 139 isExtracting = false; 133 140 summarizeBtn.disabled = false; 134 141 setSummarizeLabel("Quick Summary"); 135 - detailBtn.classList.add("hidden"); 136 142 137 143 // Check if we should auto-trigger summarize (from context menu) 138 144 const session = await chrome.storage.session.get(["triggerSummarize"]); ··· 151 157 quickSummary = ""; 152 158 detailedSummary = ""; 153 159 currentSummaryMode = "none"; 160 + chatHistory = []; 154 161 resultContainer.innerHTML = ""; 155 162 resultContainer.classList.add("hidden"); 156 163 initialState.classList.remove("hidden"); 164 + chatContainer.classList.add("hidden"); 165 + footer.classList.remove("hidden"); 166 + chatInput.value = ""; 157 167 setSummarizeLabel("Quick Summary"); 158 168 summarizeBtn.disabled = false; 159 - copyBtn.classList.add("hidden"); 160 - detailBtn.classList.add("hidden"); 161 169 isLoading = false; 170 + isChatLoading = false; 162 171 } 172 + 173 + settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage()); 163 174 164 175 summarizeBtn.addEventListener("click", async () => { 165 176 if (isLoading) return; ··· 175 186 if (quickSummary && currentTabId) { 176 187 await chrome.storage.session.remove([ 177 188 QUICK_SUMMARY_CACHE_PREFIX + currentTabId, 178 - DETAILED_SUMMARY_CACHE_PREFIX + currentTabId 189 + DETAILED_SUMMARY_CACHE_PREFIX + currentTabId, 190 + CHAT_CACHE_PREFIX + currentTabId 179 191 ]); 180 192 quickSummary = ""; 181 193 detailedSummary = ""; 194 + chatHistory = []; 182 195 } 183 196 await generateQuickSummary(); 184 197 }); 185 198 186 - detailBtn.addEventListener("click", async () => { 187 - if (isLoading) return; 188 - if (!currentPageContent) { 189 - showToast("Could not extract page content. Try refreshing."); 190 - return; 191 - } 192 - await generateDetailedSummary(); 193 - }); 194 - 195 - settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage()); 196 - 197 - copyBtn.addEventListener("click", async () => { 198 - let contentToCopy = ""; 199 - if (quickSummary) { 200 - contentToCopy += quickSummary; 201 - } 202 - if (detailedSummary) { 203 - if (contentToCopy) contentToCopy += "\n\n---\n\n"; 204 - contentToCopy += detailedSummary; 205 - } 206 - if (!contentToCopy) return; 207 - 208 - try { 209 - await navigator.clipboard.writeText(contentToCopy); 210 - document.getElementById("copy-icon-default").classList.add("hidden"); 211 - document.getElementById("copy-icon-done").classList.remove("hidden"); 212 - setTimeout(() => { 213 - document.getElementById("copy-icon-default").classList.remove("hidden"); 214 - document.getElementById("copy-icon-done").classList.add("hidden"); 215 - }, 2000); 216 - } catch (e) { 217 - showToast("Could not copy to clipboard."); 218 - } 219 - }); 220 - 221 199 async function extractPageContent() { 222 200 try { 223 201 const [tab] = await chrome.tabs.query({ ··· 257 235 initialState.classList.add("hidden"); 258 236 resultContainer.classList.remove("hidden"); 259 237 resultContainer.innerHTML = renderMarkdown(content); 260 - copyBtn.classList.remove("hidden"); 238 + 239 + // Create button container for copy and regenerate 240 + const buttonContainer = document.createElement("div"); 241 + buttonContainer.className = "summary-actions"; 242 + 243 + // Add copy button 244 + const copyBtnEl = document.createElement("button"); 245 + copyBtnEl.className = "copy-summary-btn"; 246 + copyBtnEl.innerHTML = ` 247 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 248 + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/> 249 + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/> 250 + </svg> 251 + <span>Copy</span> 252 + `; 253 + copyBtnEl.addEventListener("click", async () => { 254 + let contentToCopy = ""; 255 + if (quickSummary) { 256 + contentToCopy += quickSummary; 257 + } 258 + if (detailedSummary) { 259 + if (contentToCopy) contentToCopy += "\n\n---\n\n"; 260 + contentToCopy += detailedSummary; 261 + } 262 + if (!contentToCopy) return; 263 + 264 + try { 265 + await navigator.clipboard.writeText(contentToCopy); 266 + copyBtnEl.classList.add("copied"); 267 + copyBtnEl.innerHTML = ` 268 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 269 + <polyline points="20 6 9 17 4 12"/> 270 + </svg> 271 + <span>Copied</span> 272 + `; 273 + setTimeout(() => { 274 + copyBtnEl.classList.remove("copied"); 275 + copyBtnEl.innerHTML = ` 276 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 277 + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/> 278 + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/> 279 + </svg> 280 + <span>Copy</span> 281 + `; 282 + }, 2000); 283 + } catch (e) { 284 + showToast("Could not copy to clipboard."); 285 + } 286 + }); 287 + buttonContainer.appendChild(copyBtnEl); 288 + 289 + // Add regenerate button 290 + const regenerateBtnEl = document.createElement("button"); 291 + regenerateBtnEl.className = "copy-summary-btn"; 292 + regenerateBtnEl.innerHTML = ` 293 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 294 + <polyline points="23 4 23 10 17 10"></polyline> 295 + <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path> 296 + </svg> 297 + <span>Regenerate</span> 298 + `; 299 + regenerateBtnEl.addEventListener("click", async () => { 300 + if (isLoading) return; 301 + if (isExtracting) { 302 + showToast("Still loading page content…"); 303 + return; 304 + } 305 + if (!currentPageContent) { 306 + showToast("Could not extract page content. Try refreshing."); 307 + return; 308 + } 309 + // If regenerating, clear the cache first 310 + if (quickSummary && currentTabId) { 311 + await chrome.storage.session.remove([ 312 + QUICK_SUMMARY_CACHE_PREFIX + currentTabId, 313 + DETAILED_SUMMARY_CACHE_PREFIX + currentTabId, 314 + CHAT_CACHE_PREFIX + currentTabId 315 + ]); 316 + quickSummary = ""; 317 + detailedSummary = ""; 318 + chatHistory = []; 319 + } 320 + await generateQuickSummary(); 321 + }); 322 + buttonContainer.appendChild(regenerateBtnEl); 323 + 324 + resultContainer.appendChild(buttonContainer); 325 + 326 + // Hide footer after summary is shown 327 + footer.classList.add("hidden"); 328 + 329 + showChat(); 330 + renderChatMessages(); 331 + } 332 + 333 + function showChat() { 334 + chatContainer.classList.remove("hidden"); 335 + } 336 + 337 + function renderChatMessages() { 338 + if (chatHistory.length === 0) return; 339 + 340 + // Check if we already rendered chat messages (look for chat-divider) 341 + let chatSection = resultContainer.querySelector('.chat-section'); 342 + 343 + if (!chatSection) { 344 + // First time rendering chat - add divider and section 345 + chatSection = document.createElement('div'); 346 + chatSection.className = 'chat-section'; 347 + 348 + const divider = document.createElement('hr'); 349 + divider.className = 'chat-divider'; 350 + chatSection.appendChild(divider); 351 + 352 + resultContainer.appendChild(chatSection); 353 + } 354 + 355 + // Clear existing messages (we'll re-render all) 356 + // Keep only the divider 357 + const divider = chatSection.querySelector('.chat-divider'); 358 + chatSection.innerHTML = ''; 359 + chatSection.appendChild(divider); 360 + 361 + // Render all messages 362 + chatHistory.forEach((msg) => { 363 + const msgEl = document.createElement("div"); 364 + msgEl.className = `chat-message ${msg.role}`; 365 + msgEl.innerHTML = renderMarkdown(msg.content); 366 + chatSection.appendChild(msgEl); 367 + }); 368 + 369 + // Scroll to bottom of content-container (not just messages) 370 + const contentContainer = document.querySelector('.content-container'); 371 + contentContainer.scrollTop = contentContainer.scrollHeight; 261 372 } 262 373 263 - async function generateQuickSummary() { 264 - setLoading(true); 265 - currentSummaryMode = "quick"; 266 - initialState.classList.add("hidden"); 267 - resultContainer.classList.remove("hidden"); 268 - resultContainer.innerHTML = ` 269 - <div class="loading-wrap"> 270 - <div class="spinner"></div> 271 - <span class="loading-label">Thinking…</span> 272 - </div> 273 - `; 374 + async function sendChatMessage() { 375 + const message = chatInput.value.trim(); 376 + if (!message || isChatLoading || !currentPageContent) return; 377 + 378 + // Add user message to history 379 + chatHistory.push({ role: "user", content: message }); 380 + chatInput.value = ""; 381 + renderChatMessages(); 382 + 383 + // Show loading state 384 + isChatLoading = true; 385 + chatSendBtn.disabled = true; 386 + 387 + // Find chat section or create it 388 + let chatSection = resultContainer.querySelector('.chat-section'); 389 + if (!chatSection) { 390 + chatSection = document.createElement('div'); 391 + chatSection.className = 'chat-section'; 392 + const divider = document.createElement('hr'); 393 + divider.className = 'chat-divider'; 394 + chatSection.appendChild(divider); 395 + resultContainer.appendChild(chatSection); 396 + } 397 + 398 + const loadingEl = document.createElement("div"); 399 + loadingEl.className = "chat-message assistant loading"; 400 + loadingEl.innerHTML = '<div class="chat-spinner"></div>'; 401 + chatSection.appendChild(loadingEl); 402 + 403 + // Scroll to show loading 404 + const contentContainer = document.querySelector('.content-container'); 405 + contentContainer.scrollTop = contentContainer.scrollHeight; 274 406 275 407 try { 276 408 const settings = await chrome.storage.sync.get({ ··· 280 412 apiKey: "", 281 413 }); 282 414 283 - const pageContentForLLM = currentPageContent.substring(0, 8000); 284 - 285 - const quickSummaryPrompt = `Please provide a "Quick Summary" of this webpage. Focus on the main points and key takeaways. Use markdown formatting (headings, bullet points, etc.). 286 - 287 - The "Quick Summary" should be 3-5 **short** one-sentence bullet points. Each of these bullet points should have key points/takeaways **bolded** so people can quickly scan.`; 415 + const pageContentForLLM = currentPageContent.substring(0, 6000); 416 + const summaryContent = quickSummary + (detailedSummary ? "\n\n" + detailedSummary : ""); 288 417 418 + // Build messages for API - include recent chat history (last 4 messages for context) 419 + const recentHistory = chatHistory.slice(-5, -1); // Exclude the message we just added 289 420 const apiMessages = [ 290 - { role: "system", content: "You are a helpful assistant that summarizes webpages concisely." }, 291 - { 292 - role: "system", 293 - content: `The following is the content of the current webpage:\n\n${pageContentForLLM}`, 294 - }, 295 - { 296 - role: "user", 297 - content: quickSummaryPrompt, 298 - }, 421 + { role: "system", content: "You are a helpful assistant answering questions about a webpage. Use the provided page content and summary to give accurate, concise answers." }, 422 + { role: "system", content: `Page content:\n\n${pageContentForLLM}` }, 423 + { role: "system", content: `Summary of the page:\n\n${summaryContent}` }, 424 + ...recentHistory.map(m => ({ role: m.role, content: m.content })), 425 + { role: "user", content: message } 299 426 ]; 300 - 301 - await chrome.runtime.sendMessage({ action: "ping" }).catch(() => {}); 302 427 303 428 const response = await chrome.runtime.sendMessage({ 304 429 action: "chat", ··· 311 436 }, 312 437 }); 313 438 439 + // Remove loading indicator 440 + loadingEl.remove(); 441 + 314 442 if (!response || !response.success) { 315 443 throw new Error(response?.error || "No response from extension"); 316 444 } 317 445 318 - const summary = 446 + const answer = 319 447 response.data.choices?.[0]?.message?.content || 320 448 response.data.response || 321 449 response.data.message?.content || 322 450 "No response received"; 323 451 324 - quickSummary = summary; 325 - detailedSummary = ""; // Reset detailed summary when regenerating quick summary 326 - showSummary(quickSummary); 327 - setSummarizeLabel("Regenerate"); 328 - detailBtn.classList.remove("hidden"); 452 + // Add assistant response to history 453 + chatHistory.push({ role: "assistant", content: answer }); 454 + renderChatMessages(); 329 455 330 - // Cache the quick summary for this tab 456 + // Cache chat history 331 457 if (currentTabId) { 332 458 await chrome.storage.session.set({ 333 - [QUICK_SUMMARY_CACHE_PREFIX + currentTabId]: { 334 - summary: quickSummary, 459 + [CHAT_CACHE_PREFIX + currentTabId]: { 460 + messages: chatHistory, 335 461 url: currentTabUrl, 336 462 timestamp: Date.now() 337 - }, 338 - [CONTENT_CACHE_PREFIX + currentTabId]: { 339 - content: currentPageContent, 340 - url: currentTabUrl 341 463 } 342 464 }); 343 - // Clear any old detailed summary cache since we're regenerating 344 - await chrome.storage.session.remove([DETAILED_SUMMARY_CACHE_PREFIX + currentTabId]); 345 465 } 346 466 } catch (error) { 347 - console.error("API Error:", error); 348 - resultContainer.innerHTML = `<div class="error-message">${escapeHtml(error.message)}</div>`; 349 - detailBtn.classList.add("hidden"); 467 + // Remove loading indicator 468 + loadingEl.remove(); 469 + console.error("Chat Error:", error); 470 + 471 + // Show error in chat 472 + const errorEl = document.createElement("div"); 473 + errorEl.className = "chat-message assistant error"; 474 + errorEl.textContent = "Error: " + error.message; 475 + chatSection.appendChild(errorEl); 476 + 477 + // Scroll to show error 478 + const contentContainer = document.querySelector('.content-container'); 479 + contentContainer.scrollTop = contentContainer.scrollHeight; 350 480 } finally { 351 - setLoading(false); 481 + isChatLoading = false; 482 + chatSendBtn.disabled = false; 483 + chatInput.focus(); 352 484 } 353 485 } 354 486 355 - async function generateDetailedSummary() { 487 + // Chat event listeners 488 + chatSendBtn.addEventListener("click", sendChatMessage); 489 + chatInput.addEventListener("keypress", (e) => { 490 + if (e.key === "Enter" && !e.shiftKey) { 491 + e.preventDefault(); 492 + sendChatMessage(); 493 + } 494 + }); 495 + 496 + async function generateQuickSummary() { 356 497 setLoading(true); 357 - currentSummaryMode = "detailed"; 358 - 359 - // Show loading indicator appended to current content 360 - const currentContent = quickSummary; 361 - resultContainer.innerHTML = renderMarkdown(currentContent) + ` 362 - <div class="loading-wrap" style="margin-top: 20px;"> 498 + currentSummaryMode = "quick"; 499 + initialState.classList.add("hidden"); 500 + resultContainer.classList.remove("hidden"); 501 + resultContainer.innerHTML = ` 502 + <div class="loading-wrap"> 363 503 <div class="spinner"></div> 364 - <span class="loading-label">Generating detailed summary…</span> 504 + <span class="loading-label">Thinking…</span> 365 505 </div> 366 506 `; 367 507 ··· 375 515 376 516 const pageContentForLLM = currentPageContent.substring(0, 8000); 377 517 378 - const detailedPrompt = `Expand on this summary of the webpage, with sections that go into a bit more detail. These sections can include direct quotes from the webpage.`; 518 + const quickSummaryPrompt = `Please provide a "Quick Summary" of this webpage. Focus on the main points and key takeaways. Use markdown formatting (headings, bullet points, etc.). 519 + 520 + The "Quick Summary" should be 3-5 **short** one-sentence bullet points. Each of these bullet points should have key points/takeaways **bolded** so people can quickly scan.`; 379 521 380 522 const apiMessages = [ 381 - { role: "system", content: "You are a helpful assistant that provides detailed webpage summaries." }, 523 + { role: "system", content: "You are a helpful assistant that summarizes webpages concisely." }, 382 524 { 383 525 role: "system", 384 526 content: `The following is the content of the current webpage:\n\n${pageContentForLLM}`, 385 527 }, 386 528 { 387 - role: "assistant", 388 - content: `Quick Summary:\n${quickSummary}`, 389 - }, 390 - { 391 529 role: "user", 392 - content: detailedPrompt, 530 + content: quickSummaryPrompt, 393 531 }, 394 532 ]; 395 533 ··· 416 554 response.data.message?.content || 417 555 "No response received"; 418 556 419 - detailedSummary = summary; 420 - 421 - // Combine quick and detailed summaries 422 - const combinedContent = quickSummary + "\n\n---\n\n" + detailedSummary; 423 - showSummary(combinedContent); 557 + quickSummary = summary; 558 + detailedSummary = ""; // Reset detailed summary when regenerating quick summary 559 + showSummary(quickSummary); 424 560 setSummarizeLabel("Regenerate"); 425 - detailBtn.classList.add("hidden"); 426 561 427 - // Cache the detailed summary for this tab 562 + // Cache the quick summary for this tab 428 563 if (currentTabId) { 429 564 await chrome.storage.session.set({ 430 - [DETAILED_SUMMARY_CACHE_PREFIX + currentTabId]: { 431 - summary: detailedSummary, 565 + [QUICK_SUMMARY_CACHE_PREFIX + currentTabId]: { 566 + summary: quickSummary, 432 567 url: currentTabUrl, 433 568 timestamp: Date.now() 569 + }, 570 + [CONTENT_CACHE_PREFIX + currentTabId]: { 571 + content: currentPageContent, 572 + url: currentTabUrl 434 573 } 435 574 }); 575 + // Clear any old detailed summary cache since we're regenerating 576 + await chrome.storage.session.remove([DETAILED_SUMMARY_CACHE_PREFIX + currentTabId]); 436 577 } 437 578 } catch (error) { 438 579 console.error("API Error:", error); 439 - // Show error but keep the quick summary 440 - resultContainer.innerHTML = renderMarkdown(quickSummary) + ` 441 - <div class="error-message" style="margin-top: 20px;"> 442 - ${escapeHtml(error.message)} 443 - </div> 444 - `; 580 + resultContainer.innerHTML = `<div class="error-message">${escapeHtml(error.message)}</div>`; 445 581 } finally { 446 582 setLoading(false); 447 583 } ··· 466 602 function setLoading(loading) { 467 603 isLoading = loading; 468 604 summarizeBtn.disabled = loading; 469 - detailBtn.disabled = loading; 470 605 if (loading) { 471 606 setSummarizeLabel("Thinking…"); 472 607 } else {
+3 -1
scripts/background.js
··· 4 4 const QUICK_SUMMARY_CACHE_PREFIX = "quick_summary_cache_"; 5 5 const DETAILED_SUMMARY_CACHE_PREFIX = "detailed_summary_cache_"; 6 6 const CONTENT_CACHE_PREFIX = "content_cache_"; 7 + const CHAT_CACHE_PREFIX = "chat_cache_"; 7 8 8 9 chrome.runtime.onInstalled.addListener(() => { 9 10 // Set default settings only if they don't already exist ··· 45 46 await chrome.storage.session.remove([ 46 47 QUICK_SUMMARY_CACHE_PREFIX + tabId, 47 48 DETAILED_SUMMARY_CACHE_PREFIX + tabId, 48 - CONTENT_CACHE_PREFIX + tabId 49 + CONTENT_CACHE_PREFIX + tabId, 50 + CHAT_CACHE_PREFIX + tabId 49 51 ]); 50 52 } catch (e) { 51 53 console.error("[WebAI] Error clearing cache:", e);