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.

Add AI-generated follow-up suggestions after summary and chat responses

+257 -9
+16
popup/popup.css
··· 507 507 background: var(--bg); 508 508 } 509 509 510 + /* Suggestion skeleton placeholder */ 511 + .suggestion-skeleton { 512 + padding: 5px 10px; 513 + border-radius: 14px; 514 + border: 1px solid var(--border); 515 + background: var(--bg-subtle); 516 + width: 120px; 517 + height: 24px; 518 + animation: skeleton-pulse 1.5s ease-in-out infinite; 519 + } 520 + 521 + @keyframes skeleton-pulse { 522 + 0%, 100% { opacity: 0.4; } 523 + 50% { opacity: 0.7; } 524 + } 525 + 510 526 /* Chat messages are rendered inside .result, sharing the same scroll */ 511 527 .chat-divider { 512 528 margin: 20px 0 16px 0;
+241 -9
popup/popup.js
··· 28 28 const chatSendBtn = document.getElementById("chat-send"); 29 29 const footer = document.getElementById("footer"); 30 30 31 + let generatedSuggestions = []; // AI-generated follow-up questions based on summary 32 + 33 + const SUGGESTIONS_PROMPT = `Based on the summary provided, generate 2 natural follow-up questions that a reader might want to ask (besides "Why would this be worth reading?"). Keep questions short (5-8 words), like these examples: 34 + - What are some key quotes? 35 + - Explain this simply 36 + 37 + Return only the 2 questions, one per line, no numbering or bullet points.`; 38 + 39 + const CHAT_SUGGESTIONS_PROMPT = `Based on this chat response, generate 2 natural follow-up questions the reader might want to ask next. Keep questions short (5-8words). Make them specific to what was just discussed. Return only the 2 questions, one per line, no numbering or bullet points.`; 40 + 41 + // Always show this first 42 + const DEFAULT_SUGGESTION = "Why would this be worth reading?"; 43 + 44 + // Fallback static suggestions if AI generation fails (DEFAULT_SUGGESTION is always shown first) 31 45 const SUGGESTIONS = [ 32 - "Why would this be worth reading?", 33 46 "What are some key quotes?", 34 47 "Explain this simply", 35 48 ]; ··· 331 344 detailedSummary = ""; 332 345 currentSummaryMode = "none"; 333 346 chatHistory = []; 347 + generatedSuggestions = []; 334 348 contentWasTruncated = false; 335 349 resultContainer.innerHTML = ""; 336 350 resultContainer.classList.add("hidden"); ··· 531 545 quickSummary = ""; 532 546 detailedSummary = ""; 533 547 chatHistory = []; 548 + generatedSuggestions = []; 534 549 } 535 550 await generateQuickSummary(); 536 551 }); ··· 552 567 chatContainer.classList.remove("hidden"); 553 568 } 554 569 555 - function renderSuggestions() { 556 - if (chatHistory.length > 0) { 557 - const existing = resultContainer.querySelector(".chat-suggestions"); 558 - if (existing) existing.remove(); 559 - return; 570 + function renderSuggestions(includeDefault = true) { 571 + const existing = resultContainer.querySelector(".chat-suggestions"); 572 + if (existing) { 573 + existing.remove(); 560 574 } 561 - const existing = resultContainer.querySelector(".chat-suggestions"); 562 - if (existing) return; 575 + 576 + // Use generated suggestions if available, otherwise fall back to static 577 + const otherSuggestions = generatedSuggestions.length > 0 578 + ? generatedSuggestions 579 + : SUGGESTIONS; 580 + 581 + const allSuggestions = includeDefault 582 + ? [DEFAULT_SUGGESTION, ...otherSuggestions] 583 + : otherSuggestions; 584 + 563 585 const suggestionsEl = document.createElement("div"); 564 586 suggestionsEl.className = "chat-suggestions"; 565 - suggestionsEl.innerHTML = SUGGESTIONS.map((s) => 587 + suggestionsEl.innerHTML = allSuggestions.map((s) => 566 588 `<button class="suggestion-btn">${escapeHtml(s)}</button>` 567 589 ).join(""); 568 590 suggestionsEl.querySelectorAll(".suggestion-btn").forEach((btn) => { 569 591 btn.addEventListener("click", () => { 592 + // Remove suggestions immediately when clicked 593 + const suggestions = resultContainer.querySelector(".chat-suggestions"); 594 + if (suggestions) suggestions.remove(); 570 595 chatInput.value = btn.textContent; 571 596 sendChatMessage(); 572 597 }); ··· 737 762 chatHistory.push({ role: "assistant", content: streamingChatContent }); 738 763 renderChatMessages(); 739 764 765 + // Generate follow-up suggestions based on the conversation 766 + generateSuggestionsForChat(streamingChatContent); 767 + 740 768 // Cache chat history 741 769 if (currentTabId) { 742 770 chrome.storage.session.set({ ··· 849 877 850 878 quickSummary = streamingSummaryContent; 851 879 detailedSummary = ""; 880 + generatedSuggestions = []; // Reset suggestions 852 881 showSummary(quickSummary); 853 882 setLoading(false); // Set this after showSummary so label is correct 883 + 884 + // Generate AI suggestions based on the summary 885 + generateSuggestions(quickSummary); 854 886 855 887 // Cache the quick summary for this tab 856 888 if (currentTabId) { ··· 869 901 chrome.storage.session.remove([ 870 902 DETAILED_SUMMARY_CACHE_PREFIX + currentTabId, 871 903 ]); 904 + } 905 + } 906 + 907 + async function generateSuggestions(summary) { 908 + // Don't generate if chat already started 909 + if (chatHistory.length > 0) return; 910 + 911 + // Show loading state for suggestions 912 + renderSuggestionsLoading(); 913 + 914 + // Add timeout failsafe - if suggestions take too long, show fallback 915 + const timeoutId = setTimeout(() => { 916 + if (generatedSuggestions.length === 0) { 917 + console.log("Suggestions generation timed out, showing fallback"); 918 + generatedSuggestions = []; 919 + renderSuggestions(); 920 + } 921 + }, 15000); // 15 second timeout 922 + 923 + try { 924 + const settings = await chrome.storage.sync.get({ 925 + apiMode: "ollama", 926 + apiBaseUrl: "http://localhost:11434", 927 + model: "gpt-oss:20b-cloud", 928 + apiKey: "", 929 + disableThinking: false, 930 + }); 931 + 932 + const apiMessages = [ 933 + { role: "system", content: SYSTEM_PROMPT_SUMMARIZER }, 934 + { 935 + role: "system", 936 + content: `Summary of the article:\n\n${summary}`, 937 + }, 938 + { 939 + role: "user", 940 + content: SUGGESTIONS_PROMPT, 941 + }, 942 + ]; 943 + 944 + const response = await chrome.runtime.sendMessage({ 945 + action: "chat", 946 + data: { 947 + apiBaseUrl: settings.apiBaseUrl, 948 + model: settings.model, 949 + apiKey: settings.apiKey, 950 + messages: apiMessages, 951 + apiMode: settings.apiMode, 952 + disableThinking: settings.disableThinking, 953 + }, 954 + }); 955 + 956 + clearTimeout(timeoutId); 957 + 958 + if (!response || !response.success) { 959 + throw new Error(response?.error || "Failed to generate suggestions"); 960 + } 961 + 962 + const content = 963 + response.data.choices?.[0]?.message?.content || 964 + response.data.response || 965 + response.data.message?.content || 966 + ""; 967 + 968 + console.log("Raw suggestions response:", content); 969 + 970 + // Parse suggestions from the response (one per line) 971 + // More lenient parsing - accept lines with questions or just meaningful content 972 + generatedSuggestions = content 973 + .split("\n") 974 + .map((line) => line.trim()) 975 + .map((line) => line.replace(/^\d+[.\-)]\s*/, "")) // Remove numbering like "1. " or "1) " or "- " 976 + .filter((line) => line.length > 10) // Must be substantial 977 + .slice(0, 3); 978 + 979 + console.log("Parsed suggestions:", generatedSuggestions); 980 + 981 + // If no valid suggestions parsed, fall back to static 982 + if (generatedSuggestions.length === 0) { 983 + console.log("No valid suggestions parsed, using fallback"); 984 + generatedSuggestions = []; 985 + } 986 + 987 + // Render the generated suggestions (or fallback) 988 + renderSuggestions(); 989 + } catch (error) { 990 + clearTimeout(timeoutId); 991 + console.error("Failed to generate suggestions:", error); 992 + // Fall back to static suggestions if generation fails 993 + generatedSuggestions = []; 994 + renderSuggestions(); 995 + } 996 + } 997 + 998 + function renderSuggestionsLoading(includeDefault = true) { 999 + const existing = resultContainer.querySelector(".chat-suggestions"); 1000 + if (existing) existing.remove(); 1001 + const suggestionsEl = document.createElement("div"); 1002 + suggestionsEl.className = "chat-suggestions"; 1003 + if (includeDefault) { 1004 + suggestionsEl.innerHTML = ` 1005 + <button class="suggestion-btn">${escapeHtml(DEFAULT_SUGGESTION)}</button> 1006 + <div class="suggestion-skeleton"></div> 1007 + <div class="suggestion-skeleton"></div> 1008 + `; 1009 + } else { 1010 + suggestionsEl.innerHTML = ` 1011 + <div class="suggestion-skeleton"></div> 1012 + <div class="suggestion-skeleton"></div> 1013 + `; 1014 + } 1015 + suggestionsEl.querySelectorAll(".suggestion-btn").forEach((btn) => { 1016 + btn.addEventListener("click", () => { 1017 + // Remove suggestions immediately when clicked 1018 + const suggestions = resultContainer.querySelector(".chat-suggestions"); 1019 + if (suggestions) suggestions.remove(); 1020 + chatInput.value = btn.textContent; 1021 + sendChatMessage(); 1022 + }); 1023 + }); 1024 + resultContainer.appendChild(suggestionsEl); 1025 + } 1026 + 1027 + async function generateSuggestionsForChat(lastResponse) { 1028 + // Show loading state with skeletons (no default for chat follow-ups) 1029 + renderSuggestionsLoading(false); 1030 + 1031 + // Timeout failsafe 1032 + const timeoutId = setTimeout(() => { 1033 + if (generatedSuggestions.length === 0) { 1034 + generatedSuggestions = []; 1035 + renderSuggestions(false); 1036 + } 1037 + }, 15000); 1038 + 1039 + try { 1040 + const settings = await chrome.storage.sync.get({ 1041 + apiMode: "ollama", 1042 + apiBaseUrl: "http://localhost:11434", 1043 + model: "gpt-oss:20b-cloud", 1044 + apiKey: "", 1045 + disableThinking: false, 1046 + }); 1047 + 1048 + const apiMessages = [ 1049 + { role: "system", content: SYSTEM_PROMPT_CHAT }, 1050 + { 1051 + role: "user", 1052 + content: `${CHAT_SUGGESTIONS_PROMPT}\n\nResponse:\n${lastResponse}`, 1053 + }, 1054 + ]; 1055 + 1056 + const response = await chrome.runtime.sendMessage({ 1057 + action: "chat", 1058 + data: { 1059 + apiBaseUrl: settings.apiBaseUrl, 1060 + model: settings.model, 1061 + apiKey: settings.apiKey, 1062 + messages: apiMessages, 1063 + apiMode: settings.apiMode, 1064 + disableThinking: settings.disableThinking, 1065 + }, 1066 + }); 1067 + 1068 + clearTimeout(timeoutId); 1069 + 1070 + if (!response || !response.success) { 1071 + throw new Error(response?.error || "Failed to generate suggestions"); 1072 + } 1073 + 1074 + const content = 1075 + response.data.choices?.[0]?.message?.content || 1076 + response.data.response || 1077 + response.data.message?.content || 1078 + ""; 1079 + 1080 + console.log("Chat suggestions response:", content); 1081 + 1082 + // Parse suggestions from the response 1083 + generatedSuggestions = content 1084 + .split("\n") 1085 + .map((line) => line.trim()) 1086 + .map((line) => line.replace(/^[->•]\s*/, "")) // Remove bullet points 1087 + .map((line) => line.replace(/^\d+[.\-)]\s*/, "")) 1088 + .filter((line) => line.length > 5) 1089 + .slice(0, 2); 1090 + 1091 + console.log("Parsed chat suggestions:", generatedSuggestions); 1092 + 1093 + if (generatedSuggestions.length === 0) { 1094 + generatedSuggestions = []; 1095 + } 1096 + 1097 + renderSuggestions(false); 1098 + } catch (error) { 1099 + clearTimeout(timeoutId); 1100 + console.error("Failed to generate chat suggestions:", error); 1101 + generatedSuggestions = []; 1102 + renderSuggestions(false); 1103 + renderSuggestions(); 872 1104 } 873 1105 } 874 1106