【319号】Grok~安全Xエディタ 令和070605
Grokのウェブアプリをひねり出してもらった
X投稿用入力画面スタイルのメモ帳をGrokに頼んだ。Javascript等を使っている。
<!DOCTYPE html><html lang="ja"><head><meta charset="UTF-8"><title>安全Xエディタ</title><style>body {background-color: #000;color: #fff;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;margin: 0;display: flex;flex-direction: column;height: 100vh;-webkit-font-smoothing: antialiased;}.toolbar {background-color: #16181c;padding: 10px;border-bottom: 1px solid #2f3336;display: flex;gap: 8px;position: fixed;top: 0;left: 0;right: 0;z-index: 1000;overflow-x: auto;white-space: nowrap;align-items: center;}.toolbar button {background-color: #2f3336;color: #fff;border: none;padding: 8px 14px;cursor: pointer;font-size: 14px;border-radius: 9999px;transition: background-color 0.2s;}.toolbar button:hover {background-color: #1d9bf0;}.toolbar label {color: #71767b;font-size: 14px;display: flex;align-items: center;gap: 4px;}#fileInput, #imageInput, #htmlInput {display: none;}#charCount {font-size: 14px;color: #71767b;margin-left: 10px;}#charCount.warning {color: #f4212e;}.editor-container {flex: 1;display: flex;flex-direction: column;margin-top: 60px;padding: 16px;overflow-y: auto;}#editor {flex: 1;background-color: transparent;color: #fff;border: none;padding: 12px 0;font-size: 48pt;line-height: 1.4;resize: none;outline: none;width: 100%;box-sizing: border-box;caret-color: #1d9bf0;font-family: inherit;overflow-y: auto;}#editor::placeholder {color: #71767b;}#editor:focus {outline: none;}.image-preview {display: flex;flex-wrap: wrap;gap: 8px;margin-top: 12px;}.image-preview img {width: 120px;height: 120px;object-fit: cover;border-radius: 12px;border: 1px solid #2f3336;}.post-button {background-color: #1d9bf0;color: #fff;font-weight: bold;padding: 10px 20px;border-radius: 9999px;border: none;cursor: pointer;margin-top: 12px;align-self: flex-end;}.post-button:hover {background-color: #1a8cd8;}.post-button:disabled {background-color: #1d9bf066;cursor: not-allowed;}</style></head><body><div class="toolbar" id="toolbar"><button onclick="insertHashtag()">#</button><button onclick="toggleBold()">B</button><button onclick="toggleItalic()">I</button><button onclick="document.getElementById('imageInput').click()">📷</button><input type="file" id="imageInput" accept="image/*" multiple onchange="previewImages(event)"><button onclick="document.getElementById('fileInput').click()">TXT読込</button><input type="file" id="fileInput" accept=".txt" onchange="loadTextFile(event)"><button onclick="document.getElementById('htmlInput').click()">HTML読込</button><input type="file" id="htmlInput" accept=".html" onchange="loadHtmlFile(event)"><button onclick="saveText()">TXT保存</button><button onclick="saveHtml()">HTML保存</button><button onclick="clearDocument()">クリア</button><button onclick="copyAllText()">コピー</button><label><input type="checkbox" id="under13">13歳未満</label><span id="charCount">0/280</span></div><div class="editor-container" id="editorContainer"><textarea id="editor" placeholder="いまどうしてる?" spellcheck="false"></textarea><div class="image-preview" id="imagePreview"></div><button class="post-button" onclick="handlePost()" disabled>ポスト</button></div><script>let imageCount = 0;let imageData = [];function insertHashtag() {const editor = document.getElementById('editor');const start = editor.selectionStart;editor.setRangeText('#', start, start, 'select');updateCharCount();editor.focus();}function toggleBold() {const editor = document.getElementById('editor');const start = editor.selectionStart;const end = editor.selectionEnd;const selectedText = editor.value.slice(start, end);if (selectedText) {editor.setRangeText(`**${selectedText}**`, start, end, 'select');} else {editor.setRangeText('****', start, start, 'select');editor.selectionStart = start + 2;editor.selectionEnd = start + 2;}updateCharCount();editor.focus();}function toggleItalic() {const editor = document.getElementById('editor');const start = editor.selectionStart;const end = editor.selectionEnd;const selectedText = editor.value.slice(start, end);if (selectedText) {editor.setRangeText(`*${selectedText}*`, start, end, 'select');} else {editor.setRangeText('**', start, start, 'select');editor.selectionStart = start + 1;editor.selectionEnd = start + 1;}updateCharCount();editor.focus();}function clearDocument() {if (confirm('内容をクリアしますか?')) {const editor = document.getElementById('editor');editor.value = '';document.getElementById('imagePreview').innerHTML = '';imageCount = 0;imageData = [];updateCharCount();editor.focus();}}function loadTextFile(event) {const file = event.target.files[0];if (file) {const reader = new FileReader();reader.onload = function(e) {const editor = document.getElementById('editor');let content = e.target.result;const maxLength = 280;if (content.length > maxLength) {content = content.slice(0, maxLength);alert('最大280文字まで読み込みました。');}editor.value = content;updateCharCount();editor.focus();};reader.readAsText(file);}}function loadHtmlFile(event) {const file = event.target.files[0];if (file) {const reader = new FileReader();reader.onload = function(e) {const editor = document.getElementById('editor');const preview = document.getElementById('imagePreview');const parser = new DOMParser();const doc = parser.parseFromString(e.target.result, 'text/html');const textDiv = doc.querySelector('#postContent');let content = textDiv ? textDiv.textContent : '';const maxLength = 280;if (content.length > maxLength) {content = content.slice(0, maxLength);alert('最大280文字まで読み込みました。');}editor.value = content;imageData = [];imageCount = 0;preview.innerHTML = '';const images = doc.querySelectorAll('.image-preview img');images.forEach(img => {if (imageCount < 4 && img.src.startsWith('data:image/')) {const newImg = document.createElement('img');newImg.src = img.src;preview.appendChild(newImg);imageData.push(img.src);imageCount++;}});updateCharCount();editor.focus();};reader.readAsText(file);}}function saveText() {const content = document.getElementById('editor').value;const fileName = prompt('保存するファイル名を入力(例: post.txt)', 'post.txt');if (fileName) {const blob = new Blob([content], { type: 'text/plain' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = fileName;a.click();URL.revokeObjectURL(url);}}function saveHtml() {const content = document.getElementById('editor').value;const username = prompt('ユーザー名を入力してください(例: 太郎)', 'ユーザー名');if (!username) {alert('ユーザー名を入力してください。');return;}const handle = username.toLowerCase().replace(/\s/g, '');const fileName = prompt('保存するファイル名を入力(例: post.html)', 'post.html');if (fileName) {const escapedContent = content.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');const escapedUsername = username.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');const escapedHandle = handle.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');const imageHtml = imageData.map(src => `<img src="${src}" alt="Attached image">`).join('');const htmlContent = `<!DOCTYPE html><html lang="ja"><head><meta charset="UTF-8"><title>安全X</title><style>body {background-color: #000;color: #fff;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;padding: 20px;max-width: 600px;margin: 0 auto;}.post-container {border-bottom: 1px solid #2f3336;padding: 15px 0;}.post-header {display: flex;align-items: center;gap: 10px;margin-bottom: 10px;}.avatar {width: 48px;height: 48px;border-radius: 50%;background-color: #1d9bf0; /* 青一色 */}.username {font-weight: bold;font-size: 15px;}.handle {color: #71767b;font-size: 15px;}#postContent {font-size: 48pt;line-height: 1.4;white-space: pre-wrap;margin-bottom: 12px;}.image-preview {display: grid;grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));gap: 8px;margin-bottom: 12px;}.image-preview img {width: 100%;max-height: 300px;object-fit: cover;border-radius: 12px;border: 1px solid #2f3336;}.post-actions {display: flex;gap: 40px;color: #71767b;font-size: 13px;}.action-item {display: flex;align-items: center;gap: 8px;}</style></head><body><div class="post-container"><div class="post-header"><div class="avatar"></div><div><span class="username">${escapedUsername}</span><span class="handle">@${escapedHandle}</span></div></div><div id="postContent">${escapedContent}</div><div class="image-preview">${imageHtml}</div><div class="post-actions"><div class="action-item">0 リツイート</div><div class="action-item">0 いいね</div></div></div></body></html>`;const blob = new Blob([htmlContent], { type: 'text/html' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = fileName;a.click();URL.revokeObjectURL(url);}}function copyAllText() {const content = document.getElementById('editor').value;if (!content.trim()) {alert('コピーするテキストがありません。');return;}navigator.clipboard.writeText(content).then(() => {alert('テキストをコピーしました。');}).catch(e => {alert('コピーに失敗しました。');});document.getElementById('editor').focus();}function previewImages(event) {const files = event.target.files;const preview = document.getElementById('imagePreview');const maxImages = 4;for (let i = 0; i < files.length && imageCount < maxImages; i++) {const file = files[i];if (file.type.startsWith('image/')) {const reader = new FileReader();reader.onload = function(e) {const img = document.createElement('img');img.src = e.target.result;preview.appendChild(img);imageData.push(e.target.result);imageCount++;updateCharCount();};reader.readAsDataURL(file);}}if (files.length + imageCount > maxImages) {alert(`最大${maxImages}枚まで添付できます。`);}}function handlePost() {const under13 = document.getElementById('under13').checked;const content = document.getElementById('editor').value;if (!content.trim()) {alert('投稿するテキストがありません。');return;}if (under13) {alert('13歳未満のため、投稿はHTMLファイルとして保存されます。');saveHtml();return;}const platform = prompt('投稿先を選択してください(X または Bluesky)', 'X');if (platform && platform.toLowerCase() === 'bluesky') {shareToBluesky();} else {shareToX();}}function shareToX() {const content = document.getElementById('editor').value;const shareText = content.slice(0, 280);const shareUrl = `https://x.com/intent/tweet?text=${encodeURIComponent(shareText)}`;window.open(shareUrl, '_blank');}function shareToBluesky() {const content = document.getElementById('editor').value;const shareText = content.slice(0, 300);const shareUrl = `https://bsky.app/intent/compose?text=${encodeURIComponent(shareText)}`;window.open(shareUrl, '_blank');}function updateCharCount() {const editor = document.getElementById('editor');const content = editor.value;const count = content.length;const maxLength = 280;const charCount = document.getElementById('charCount');const postButton = document.querySelector('.post-button');if (count > maxLength) {editor.value = content.slice(0, maxLength);alert('最大280文字までです。');}charCount.textContent = `${count}/280`;charCount.classList.toggle('warning', count >= maxLength);postButton.disabled = count === 0 && imageCount === 0;editor.focus();}document.getElementById('editor').addEventListener('input', updateCharCount);document.getElementById('editor').focus();updateCharCount();</script></body></html>
このソースをテキストエディタ、メモ帳に貼り付けて、適当なファイル名に拡張子(.html)をつけて保存する。オフラインでもブラウザが立ち上がる。
13歳未満にチェックを入れ、ポストするをクリックするとHTMLファイル保存するようにしている。子どもにオフライン状態でPCを与えてチェックしておくとXごっこ。保存したHTMLファイルは日記帳の代わりにもなる。
オフラインだから、ネットは早いかなと思ってもこれだと安全。オフラインで遊べるツールが生成できたら紹介したい。