Skip to content

Instantly share code, notes, and snippets.

@vexus2
Last active July 11, 2024 12:49
Show Gist options
  • Save vexus2/2de502b0a7d272ded35715eafdbb0751 to your computer and use it in GitHub Desktop.
Save vexus2/2de502b0a7d272ded35715eafdbb0751 to your computer and use it in GitHub Desktop.
chrome.action.onClicked.addListener((tab) => {
chrome.tabs.sendMessage(tab.id, { action: "summarizeAndShowModal" }, (response) => {
if (chrome.runtime.lastError) {
console.error("Error sending message:", chrome.runtime.lastError);
} else if (response && response.status === "received") {
console.log("Message received by content script");
}
});
});
chrome.commands.onCommand.addListener((command) => {
if (command === "_execute_action") {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
if (tabs[0]) {
chrome.tabs.sendMessage(tabs[0].id, { action: "summarizeAndShowModal" }, (response) => {
if (chrome.runtime.lastError) {
console.error("Error sending message:", chrome.runtime.lastError);
} else if (response && response.status === "received") {
console.log("Message received by content script");
}
});
}
});
}
});
function parseMarkdown(markdown) {
markdown = markdown.replace(/^### (.*$)/gim, '<h3>$1</h3>');
markdown = markdown.replace(/^## (.*$)/gim, '<h2>$1</h2>');
markdown = markdown.replace(/^# (.*$)/gim, '<h1>$1</h1>');
markdown = markdown.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>');
markdown = markdown.replace(/^\* (.*$)/gim, '<ul><li>$1</li></ul>');
markdown = markdown.replace(/<\/ul><ul>/gim, '');
markdown = markdown.replace(/^\> (.*$)/gim, '<blockquote>$1</blockquote>');
markdown = markdown.replace(/\n/gim, '<br>');
return markdown;
}
var loadingElement = document.createElement('div');
loadingElement.id = 'loading-indicator';
loadingElement.style.cssText = `
display: none;
position: fixed;
top: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
border-radius: 5px;
z-index: 1002;
font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', 'Meiryo', 'Yu Gothic', sans-serif;
font-size: 14px;
`;
loadingElement.textContent = '要約中...';
document.body.appendChild(loadingElement);
// ローディング表示の関数
function showLoading() {
loadingElement.style.display = 'block';
}
// ローディング非表示の関数
function hideLoading() {
loadingElement.style.display = 'none';
}
var modal = document.createElement('div');
modal.id = 'summary-modal';
modal.style.cssText = `
display: none;
position: fixed;
z-index: 1000;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 20px;
width: 520px;
max-height: 590px;
overflow-y: auto;
font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', 'Meiryo', 'Yu Gothic', sans-serif;
line-height: 1.8;
font-size: 16px;
border: 2px solid #333;
color: #333;
left: 20px;
bottom: 20px;
`;
var modalContent = document.createElement('div');
modalContent.id = 'modal-content';
modalContent.style.cssText = `
word-wrap: break-word;
`;
modal.appendChild(modalContent);
var speakButton = document.createElement('button');
speakButton.textContent = '読み上げ開始';
speakButton.style.cssText = `
position: absolute;
bottom: 10px;
right: 10px;
padding: 5px 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
speakButton.onclick = () => {
const text = modalContent.innerText;
speakTextFast(text);
};
modal.appendChild(speakButton);
var closeButton = document.createElement('button');
closeButton.innerHTML = '&times;';
closeButton.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
background-color: transparent;
border: none;
font-size: 24px;
cursor: pointer;
padding: 0;
line-height: 1;
color: #333;
`;
closeButton.onclick = () => {
modal.style.display = 'none';
stopSpeaking();
};
modal.appendChild(closeButton);
document.body.appendChild(modal);
document.addEventListener('keydown', function(event) {
var isCmdOrCtrl = event.metaKey || event.ctrlKey;
if (isCmdOrCtrl && event.shiftKey && event.key === 'S') {
event.preventDefault();
summarizeAndShowModal(event);
}
});
async function summarizeAndShowModal(event) {
try {
showLoading();
var text = document.body.innerText;
showModal("", event); // 空のモーダルを表示
const summary = await getStreamingSummaryFromChatGPT(text, (partialSummary) => {
updateModal(partialSummary);
});
hideLoading();
updateModal(summary, true);
} catch (error) {
console.error("Error in summarizeAndShowModal:", error);
hideLoading();
showModal("申し訳ありません。要約の生成中にエラーが発生しました。", event);
}
}
function updateModal(content, isFinal = false) {
const modalContent = document.getElementById('modal-content');
modalContent.innerHTML = parseMarkdown(content);
if (isFinal) {
const style = document.createElement('style');
style.textContent = `
#modal-content h2, #modal-content h3 { margin-top: 10px; margin-bottom: 5px; color: #333; }
#modal-content ul { padding-left: 20px; color: #333; }
#modal-content blockquote { border-left: 3px solid #ccc; padding-left: 10px; margin-left: 0; color: #555; }
#modal-content p { color: #333; }
`;
modalContent.appendChild(style);
}
}
async function getStreamingSummaryFromChatGPT(text, onPartialResponse) {
const API_KEY = 'YOUR_KEY_HERE';
const API_URL = 'https://api.openai.com/v1/chat/completions';
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
model: "gpt-4o",
messages: [
{role: "system", content: "You are a helpful assistant that summarizes text."},
{role: "user", content: `
- 文章の中で特に特徴的な部分と、性能向上した部分を必ず冒頭で話す
- 口調は親しみやすく友達に話すような感じで
以下の文章をそのようにまとめよ: ${text}`}
],
stream: true
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let fullSummary = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
const parsedLines = lines
.map(line => line.replace(/^data: /, '').trim())
.filter(line => line !== '' && line !== '[DONE]')
.map(line => JSON.parse(line));
for (const parsedLine of parsedLines) {
const { choices } = parsedLine;
const { delta } = choices[0];
const { content } = delta;
if (content) {
fullSummary += content;
onPartialResponse(fullSummary);
}
}
}
return fullSummary;
}
function showModal(content, event) {
var modal = document.getElementById('summary-modal');
var modalContent = document.getElementById('modal-content');
modalContent.innerHTML = parseMarkdown(content);
const style = document.createElement('style');
style.textContent = `
#modal-content h2, #modal-content h3 { margin-top: 10px; margin-bottom: 5px; }
#modal-content ul { padding-left: 20px; }
#modal-content blockquote { border-left: 3px solid #ccc; padding-left: 10px; margin-left: 0; color: #666; }
`;
modal.appendChild(style);
modal.style.display = 'block';
}
function speakTextFast(text) {
stopSpeaking();
// テキストを1000文字ごとに分割
const chunks = text.match(/.{1,1000}/g) || [];
chunks.forEach((chunk, index) => {
const utterance = new SpeechSynthesisUtterance(chunk);
utterance.rate = 2.0;
utterance.lang = 'ja-JP';
if (index === 0) {
utterance.onstart = () => {
console.log('Speech started');
speakButton.textContent = '読み上げ停止';
speakButton.onclick = stopSpeaking;
};
}
if (index === chunks.length - 1) {
utterance.onend = () => {
console.log('Speech ended');
speakButton.textContent = '読み上げ開始';
speakButton.onclick = () => speakTextFast(text);
};
}
speechSynthesis.speak(utterance);
});
}
function stopSpeaking() {
speechSynthesis.cancel();
speakButton.textContent = '読み上げ開始';
speakButton.onclick = () => {
const text = modalContent.innerText;
speakTextFast(text);
};
}
modal.addEventListener('click', (event) => {
event.stopPropagation();
});
document.addEventListener('click', (event) => {
if (modal.style.display === 'block') {
event.preventDefault();
}
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "summarizeAndShowModal") {
summarizeAndShowModal();
}
sendResponse({status: "received"});
return true;
});
{
"manifest_version": 3,
"name": "Fast Page Summarizer and Reader",
"version": "1.3",
"description": "Summarizes the current web page using ChatGPT, displays it in a modal, and optionally reads it aloud",
"permissions": [
"activeTab",
"scripting",
"commands"
],
"host_permissions": [
"https://api.openai.com/*"
],
"action": {
"default_title": "Summarize Page"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+S",
"mac": "Command+Shift+S"
},
"description": "Summarize and show modal"
}
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"content.js"
],
"run_at": "document_idle"
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment