Skip to content

Instantly share code, notes, and snippets.

@hitode909
Last active April 3, 2025 11:34
Show Gist options
  • Save hitode909/67a477adcce60a597db6d2e343147b88 to your computer and use it in GitHub Desktop.
Save hitode909/67a477adcce60a597db6d2e343147b88 to your computer and use it in GitHub Desktop.
// ==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