Last active
July 11, 2024 12:49
-
-
Save vexus2/2de502b0a7d272ded35715eafdbb0751 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
}); | |
} | |
}); | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = '×'; | |
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; | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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