Last active
April 3, 2025 11:34
-
-
Save hitode909/67a477adcce60a597db6d2e343147b88 to your computer and use it in GitHub Desktop.
This file contains 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
// ==UserScript== | |
// @name Blog Dolphin for Hatena | |
// @namespace http://tampermonkey.net/ | |
// @version 1.0 | |
// @description はてなブログ編集画面で、ChatGPTが本文を批評してくれるイルカ | |
// @match https://blog.hatena.ne.jp/* | |
// @grant GM_registerMenuCommand | |
// @grant GM_xmlhttpRequest | |
// @grant GM_setValue | |
// @grant GM_getValue | |
// @connect api.openai.com | |
// ==/UserScript== | |
(function () { | |
'use strict'; | |
if (window.top !== window.self) return; | |
let dolphinContainer = null; | |
let lastContent = ''; | |
let stableTimer = null; | |
const FIXED_PROMPT = '日記の内容を端的に批評してください。応答は最大300文字くらいにしてください。'; | |
const DEFAULT_SUFFIX = '他に書くべき内容なども提案してください。'; | |
GM_registerMenuCommand('📝 追加プロンプトを編集', async () => { | |
const current = await GM_getValue('customPromptSuffix', DEFAULT_SUFFIX); | |
const input = prompt('ChatGPTへの追加指示を入力してください:', current); | |
if (input !== null) { | |
await GM_setValue('customPromptSuffix', input); | |
alert('追加プロンプトを更新しました。'); | |
} | |
}); | |
window.addEventListener('load', async () => { | |
let apiKey = await GM_getValue('apiKey'); | |
if (!apiKey) { | |
apiKey = prompt('OpenAI APIキーを入力してください(次回以降保存されます)'); | |
if (!apiKey) { | |
showDolphinMessage('APIキーが入力されていません。'); | |
return; | |
} | |
await GM_setValue('apiKey', apiKey); | |
} | |
const textarea = findTargetTextarea(); | |
if (textarea) { | |
showDolphinMessage('textarea を監視中…'); | |
observeTextarea(textarea, apiKey); | |
} else if (typeof tinymce !== 'undefined' && tinymce.get('body')) { | |
showDolphinMessage('TinyMCE を監視中…'); | |
observeTinyMCE(apiKey); | |
} else { | |
showDolphinMessage('textareaもTinyMCEも見つかりませんでした。'); | |
} | |
}); | |
function findTargetTextarea() { | |
const el = document.querySelector('textarea#body'); | |
if (!el) return null; | |
const style = window.getComputedStyle(el); | |
if (style.display === 'none' || style.visibility === 'hidden' || el.offsetParent === null) return null; | |
return el; | |
} | |
function observeTextarea(textarea, apiKey) { | |
textarea.addEventListener('input', () => { | |
triggerStableCheck(() => textarea.value, apiKey); | |
}); | |
} | |
function observeTinyMCE(apiKey) { | |
setInterval(() => { | |
const editor = tinymce.get('body'); | |
if (!editor) return; | |
const current = editor.getContent({ format: 'text' }); | |
triggerStableCheck(() => current, apiKey); | |
}, 1000); | |
} | |
function triggerStableCheck(getContentFn, apiKey) { | |
const current = getContentFn(); | |
if (current === lastContent) return; | |
lastContent = current; | |
if (stableTimer) clearTimeout(stableTimer); | |
stableTimer = setTimeout(() => { | |
runCritique(current, apiKey); | |
}, 3000); | |
} | |
async function runCritique(content, apiKey) { | |
showDolphinMessage('…'); | |
const suffix = await GM_getValue('customPromptSuffix', DEFAULT_SUFFIX); | |
const prompt = `${FIXED_PROMPT}\n${suffix}\n\n${content}`; | |
GM_xmlhttpRequest({ | |
method: 'POST', | |
url: 'https://api.openai.com/v1/chat/completions', | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': `Bearer ${apiKey}` | |
}, | |
data: JSON.stringify({ | |
model: 'gpt-4o', | |
messages: [{ role: 'user', content: prompt }] | |
}), | |
onload: function (response) { | |
const res = JSON.parse(response.responseText); | |
const content = res.choices?.[0]?.message?.content ?? 'No response'; | |
showDolphinMessage(content); | |
}, | |
onerror: function (err) { | |
showDolphinMessage('エラー: ' + err.message); | |
} | |
}); | |
} | |
function showDolphinMessage(msg) { | |
if (!dolphinContainer) { | |
dolphinContainer = document.createElement('div'); | |
dolphinContainer.style.position = 'fixed'; | |
dolphinContainer.style.bottom = '20px'; | |
dolphinContainer.style.right = '20px'; | |
dolphinContainer.style.zIndex = 9999; | |
dolphinContainer.style.display = 'flex'; | |
dolphinContainer.style.alignItems = 'flex-end'; | |
dolphinContainer.className = 'tamper-dolphin-bubble'; | |
dolphinContainer.style.cursor = 'move'; | |
const dolphin = document.createElement('div'); | |
dolphin.textContent = '🐬'; | |
dolphin.style.fontSize = '2.5em'; | |
dolphin.style.marginRight = '0.5em'; | |
dolphin.style.textShadow = '2px 2px 4px rgba(0,0,0,0.3)'; | |
const bubble = document.createElement('div'); | |
bubble.className = 'bubble'; | |
bubble.style.background = '#e0f7fa'; | |
bubble.style.border = '1px solid #26c6da'; | |
bubble.style.borderRadius = '10px'; | |
bubble.style.padding = '0.8em'; | |
bubble.style.maxWidth = '300px'; | |
bubble.style.maxHeight = '300px'; | |
bubble.style.overflow = 'auto'; | |
bubble.style.boxShadow = '2px 2px 10px rgba(0,0,0,0.2)'; | |
bubble.style.whiteSpace = 'pre-wrap'; | |
bubble.style.wordBreak = 'break-word'; | |
dolphinContainer.appendChild(dolphin); | |
dolphinContainer.appendChild(bubble); | |
document.body.appendChild(dolphinContainer); | |
makeDraggable(dolphinContainer); | |
} | |
const bubble = dolphinContainer.querySelector('.bubble'); | |
bubble.textContent = msg; | |
} | |
function makeDraggable(el) { | |
let isDragging = false; | |
let startX, startY, initialLeft, initialTop; | |
el.addEventListener('mousedown', (e) => { | |
isDragging = true; | |
startX = e.clientX; | |
startY = e.clientY; | |
const rect = el.getBoundingClientRect(); | |
initialLeft = rect.left; | |
initialTop = rect.top; | |
e.preventDefault(); | |
}); | |
document.addEventListener('mousemove', (e) => { | |
if (!isDragging) return; | |
const dx = e.clientX - startX; | |
const dy = e.clientY - startY; | |
el.style.left = `${initialLeft + dx}px`; | |
el.style.top = `${initialTop + dy}px`; | |
el.style.bottom = 'auto'; | |
el.style.right = 'auto'; | |
}); | |
document.addEventListener('mouseup', () => { | |
isDragging = false; | |
}); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment