Skip to content

Commit

Permalink
Enhance AI summary functionality by adding input for user questions, …
Browse files Browse the repository at this point in the history
…improving summary generation logic, and refining CSS styles for better presentation and usability
  • Loading branch information
tztsai committed Nov 10, 2024
1 parent 06a7e56 commit 9fb046f
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 62 deletions.
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- [ ] Remove details IDs and use header IDs instead
25 changes: 25 additions & 0 deletions content/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,31 @@ details summary {
}
}

div.ai-qa {
max-height: 200px;
overflow: auto;
border: 3px solid #9c9ea2;
border-radius: 6px;
input {
padding: 10px;
border: none;
border-bottom: 2px solid #9c9ea2;
border-radius: inherit;
font-size: 16px;
&:focus {
outline: none;
border-bottom-color: #0078d4; /* Change the color to your preference */
}
}
p {
padding: 10px;
margin: 0;
border-bottom: 2px solid #9c9ea2;
font-weight: lighter;
font-size: 14px;
}
}

#_html, #_toc {
word-wrap: break-word;
visibility: hidden;
Expand Down
28 changes: 12 additions & 16 deletions content/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,11 @@ var update = (update) => {
if (state.content.toc) {
toc = document.getElementById('_toc');
toc && toc.querySelectorAll('a').forEach((el) => {
toc.addEventListener('click', (e) => {
e.preventDefault();
el.addEventListener('click', (e) => {
h = document.getElementById(e.target.getAttribute('href').slice(1))
d = h.parentElement.parentElement;
if (d.tagName === 'DETAILS') {
d.open = true;
focusOnDetails(d, 'center');
}
})
})
Expand Down Expand Up @@ -137,10 +135,8 @@ var makeFoldable = (selector = 'h1, h2, h3, h4, h5') => {
if (parent.tagName === 'SUMMARY') return;

const details = document.createElement('details');
const summary = document.createElement('summary');
details.appendChild(summary);

if (header.tagName < 'H4') {
if (header.tagName <= 'H3') {
details.id = 'id-' + Math.random().toString(36).substring(2, 7);
}

Expand All @@ -157,11 +153,11 @@ var makeFoldable = (selector = 'h1, h2, h3, h4, h5') => {
}

parent.replaceChild(details, header);
summary.insertAdjacentElement('afterbegin', header);
details.insertAdjacentElement('afterbegin', header);

// Expand a details element when hovering over it
header.addEventListener('mouseenter', () => {
!isScrolling && focusOnDetails(details);
!scrollLock && focusOnDetails(details);
});
// details.addEventListener('click', () => {
// focusedDetails = details;
Expand All @@ -171,19 +167,19 @@ var makeFoldable = (selector = 'h1, h2, h3, h4, h5') => {
}

var focusedDetails;
var isScrolling = false;
var scrollLock = false;

var focusOnDetails = (details, scroll = 'follow') => {
if (
isScrolling && focusedDetails
scrollLock && focusedDetails
&& findNext(details) !== focusedDetails
&& findPrev(details) !== focusedDetails
) {
focusedDetails = details;
return;
}

isScrolling = true;
scrollLock = true;
isDownward = true;
isNewSection = false;
details.open = true;
Expand All @@ -202,7 +198,7 @@ var focusOnDetails = (details, scroll = 'follow') => {
}
focusedDetails = details;

setTimeout(() => { isScrolling = false; }, 500);
setTimeout(() => { scrollLock = false; }, 500);

if (scroll == 'center') {
return details.scrollIntoView({ behavior: 'smooth', block: 'center' });
Expand All @@ -213,16 +209,16 @@ var focusOnDetails = (details, scroll = 'follow') => {
// keep the top of the section at the same position
dy = rect2.top - rect1.top;
if (isNewSection && isDownward) {
dy += rect2.height + 30;
focusedDetails = details.nextElementSibling;
// dy += rect2.height + 30;
// focusedDetails = details.nextElementSibling;
}
else if (isDownward) { // move towards the end of the block
dy += Math.max(Math.min(rect1.top, rect1.height - 50), 0);
}

if (Math.abs(dy) > 50) {
if (Math.abs(dy) > 200) {
window.scrollBy({ top: dy, behavior: 'smooth' });
} else isScrolling = false;
} else scrollLock = false;
}

function findNext(elm) {
Expand Down
173 changes: 127 additions & 46 deletions content/mdwise.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,134 @@ function cleanHtml() {
}

async function generateSummaries(text) {
// const prompt = `Convert the following text provided by the user to a well-structured Markdown document. For large chunks of text, consider splitting them into smaller subsections. For each section of any level containing too much information for the user to easily digest, **write a brief summary under its header with prefix "> Summary: "**. Do your best to enable the user to clearly and quickly understand the whole document from top level to bottom.`;
const prompt = `In the given HTML document, for each <details> element, if necessary, write a brief summary of its content. Also consider adding links in your summary to relevant headers in this document.
const prompt = `For each <details> element with an ID at different levels in the provided HTML document, if it contains a long text, generate a concise summary of its content. You can include <a> links in your summary that reference relevant headers within the document.
Your response must strictly follow this format (each summary separated by two new lines):
Each summary should strictly follow the format \`#ID: Summary\`, with each one separated by **two new lines**. Ensure there are no unnecessary HTML tags or triple backticks in your response. Never put two summaries in the same line.
ID: Summary
**Examples:**
ID: Summary
#id-abcde: A brief summary of the content within <details id="id-abcde">...</details>.
The ID is the "id" attribute of the <details> element, e.g. "id-12345".
#id-12345: Summary of id-12345. Refer to <a href="#a-header">A Header</a>.
`

const fillSummary = (output, done) => {
// output = output.replace(/\s*```\s*[a-z]*\s*/g, '');
splits = output.trim().split(/\n{2,}/);
var i = 0;
while (i < splits.length - !done) {
if (!splits[i].trim()) break;
try {
const [_, id, txt] = splits[i].match(/\s*#(id-\w+): ([\s\S]+)/m)
const d = document.getElementById(id);
if (d) {
writeSummary(d, txt);
} else {
console.error('Invalid ID:', id)
}
i += 1;
} catch (error) {
console.error('Invalid format:', splits[i]);
break;
}
}
if (done && splits.length > 1)
console.error('Error parsing:', output);
return splits.slice(i).join('\n\n');
}

await getAIResponse(text, prompt, fillSummary);
};

function writeSummary(details, txt) {
if (!txt) {
var summary = document.createElement('summary');
// move header into summary
details.insertBefore(summary, details.firstChild);
summary.appendChild(details.children[1]);
} else {
var summary = details.querySelector('summary');

const p = document.createElement('blockquote');
p.textContent = txt.trim();
summary.appendChild(p);

const qa = document.createElement('div');
qa.className = 'ai-qa';
qa.style.display = 'none';
summary.appendChild(qa);

function createInput() {
const input = document.createElement('input');
input.placeholder = 'Ask AI';
qa.insertAdjacentElement('afterbegin', input);

input.addEventListener('keydown', async (e) => {
console.warn(JSON.stringify(e));

e.stopPropagation();
if (e.key === ' ') {
e.preventDefault(); input.value += ' ';
}

if (e.key === 'Enter' && input.value.trim()) {
// remove all siblings following the input
while (input.previousElementSibling)
input.previousElementSibling.remove();

const question = input.value.trim();
const prompt = `Write a short answer given the following information:
${details.textContent}`;

const ans = document.createElement('p');
qa.insertBefore(ans, input.nextElementSibling);

await getAIResponse(question, prompt,
(output, _) => {
return ans.textContent = output;
});

createInput();
// m.redraw();
update();
}
});
return input;
}

summary.addEventListener('mouseenter', () => {
qa.style.display = 'block';
});
summary.addEventListener('mouseleave', () => {
qa.style.display = 'none';
});
createInput();
}
}

async function getAIResponse(text, prompt, callback = (s, d) => s) {

const messageJson = {
model: "gpt-4o-mini",
messages: [
{ role: 'system', content: prompt },
{ role: 'user', content: text },
],
messages: [{ role: 'user', content: text }],
stream: true,
max_tokens: 4096,
// temperature: 0.1,
temperature: 0.1,
};

if (prompt) {
messageJson.messages.unshift({ role: 'system', content: prompt });
}

const apiKey = await new Promise((resolve) => {
chrome.storage.local.get('openaiApiKey', (result) => {
resolve(result.openaiApiKey);
});
});

console.warn('Generation started.\nQuery:', text.slice(0, 300),
'\nPrompt:', prompt.slice(0, 500));

const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
Expand All @@ -63,33 +162,7 @@ async function generateSummaries(text) {
var buf = '';
var done = false;

const fillSummary = () => {
output = output.replace(/\s*```\s*[a-z]*\s*/g, '');
splits = output.trim().split(/\n{2,}/);
var i = 0;
while (i < splits.length - !done) {
if (!splits[i].trim()) break;
try {
const [_, id, txt] = splits[i].match(/\s*(id-\w+): ([\s\S]+)/m)
const d = document.getElementById(id);
if (d) {
const p = document.createElement('blockquote');
p.textContent = txt.trim();
d.querySelector('summary').appendChild(p);
// focusOnDetails(d);
} else {
console.error('Invalid ID:', id)
}
i += 1;
} catch (error) {
console.error('Invalid format:', splits[i]);
break;
}
}
output = splits.slice(i).join('\n\n');
}

var interval = setInterval(fillSummary, 200);
var interval = setInterval(() => { output = callback(output, done) }, 200);

while (true) {
var { done, value } = await reader.read();
Expand All @@ -114,11 +187,13 @@ async function generateSummaries(text) {
}

setTimeout(() => {
if (output + buf)
console.error('Error parsing:', output + buf);
if (buf) console.error('Error parsing:', buf);
clearInterval(interval);
console.warn('Generation complete.');
}, 1000);
};

return output;
}

(async () => {
// clean the HTML content
Expand All @@ -136,15 +211,21 @@ async function generateSummaries(text) {
var timeout = setInterval(() => {
if (document.readyState === 'complete') {
clearInterval(timeout);
content = document.getElementById('_html');
if (!content) return;
for (
s = content.querySelector('h1')?.previousElementSibling;
let html = document.getElementById('_html');
if (!html) return;

for ( // remove all elements before the first header
s = html.querySelector('h1')?.previousElementSibling;
s; s = s.previousElementSibling
) { s.remove(); }
const content = html.innerHTML;

// add summaries to the details elements
html.querySelectorAll('details').forEach(d => writeSummary(d));

// convert the markdown content to text
if (state.content.ai)
generateSummaries(content.outerHTML);
generateSummaries(content);
}
}, 500);
})()

0 comments on commit 9fb046f

Please sign in to comment.