<!DOCTYPE html>
<html lang="ja" data-theme="light" style="--cols:3; --card-h:640px; --accent:#2563eb">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Xlog Pro — HTMLだけで動く自動ツイートまとめ</title>
<link rel="preconnect" href="https://platform.twitter.com" crossorigin>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"/>
<meta name="description" content="HTMLだけ/APIキー不要のX(Twitter)まとめボード。プロフィール・ハッシュタグ・検索・リストを好きな列で配置し、JSON/HTML書き出しや手動ランキング、ボード切替に対応。">
<style>
:root{
--bg: #0b0e14; --panel:#111827; --muted:#9aa4b2; --text:#e5e7eb; --border:#1f2937; --chip:#141a23; --card:#0f172a; --btn:#1f2937; --btn-text:#e5e7eb; --link:#60a5fa;
}
[data-theme="light"]{ --bg:#f8fafc; --panel:#ffffff; --muted:#64748b; --text:#0f172a; --border:#e2e8f0; --chip:#f1f5f9; --card:#ffffff; --btn:#0f172a; --btn-text:#ffffff; --link:#1d9bf0; }
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji"}
header{position:sticky;top:0;z-index:10;background:var(--panel);border-bottom:1px solid var(--border)}
.wrap{max-width:1280px;margin:0 auto;padding:12px 16px}
.row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.brand{display:flex;gap:10px;align-items:center;font-weight:800}
.brand i{color:var(--accent)}
.muted{color:var(--muted)}
.pill{display:inline-flex;gap:8px;align-items:center;background:var(--chip);border:1px solid var(--border);border-radius:999px;padding:6px 10px}
.input, select, textarea{background:transparent;border:1px solid var(--border);border-radius:10px;padding:8px 10px;color:var(--text)}
textarea{min-height:88px;width:100%;}
input[type="text"].input{min-width:220px}
button{cursor:pointer;border:none}
.btn{background:var(--btn);color:var(--btn-text);padding:9px 12px;border-radius:12px}
.btn.secondary{background:transparent;color:var(--text);border:1px solid var(--border)}
.btn.ghost{background:transparent;color:var(--text)}
.btn.badge{padding:6px 10px;border-radius:999px}
.grid{display:grid;grid-template-columns:320px 1fr;gap:16px}
@media (max-width:1080px){.grid{grid-template-columns:1fr}}
aside{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:14px;position:sticky;top:72px;height:max-content}
h2{margin:6px 0 12px 0;font-size:18px}
.list{display:flex;flex-direction:column;gap:12px}
.card{background:var(--card);border:1px solid var(--border);border-radius:16px;overflow:hidden}
.card .head{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-bottom:1px solid var(--border)}
.card .head .title{display:flex;gap:8px;align-items:center;font-weight:700}
.card .body{padding:0;min-height:var(--card-h)}
.sources{display:flex;flex-wrap:wrap;gap:8px}
.chip{background:var(--chip);border:1px solid var(--border);border-radius:999px;padding:6px 10px;display:flex;gap:8px;align-items:center}
.chip b{color:var(--accent)}
.columns{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:16px}
@media (max-width:1200px){:root{--cols:2}}
@media (max-width:860px){:root{--cols:1}}
.drag{cursor:grab}
.toolbar{display:flex;gap:8px;flex-wrap:wrap}
.footer{padding:24px 16px;color:var(--muted);text-align:center}
.kbd{font-family:ui-monospace, Menlo, Monaco, Consolas; background:var(--chip); border:1px solid var(--border); padding:2px 6px; border-radius:6px}
.danger{color:#ef4444}
.accent{color:var(--accent)}
.section{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:14px}
.help{font-size:13px;color:var(--muted)}
.label{font-size:12px;color:var(--muted)}
.tiny{font-size:12px}
.row-wrap{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.w-100{width:100%}
.space{height:8px}
</style>
</head>
<body>
<header>
<div class="wrap row">
<div class="brand"><i class="fa-solid fa-wave-square"></i> Xlog <span class="muted">Pro</span></div>
<div class="pill">
<i class="fa-solid fa-diagram-project"></i>
<select id="boardSelect" title="ボード切替"></select>
<button id="boardNew" class="btn badge secondary" title="新規ボード"><i class="fa-solid fa-plus"></i></button>
<button id="boardRename" class="btn badge secondary" title="名前変更"><i class="fa-solid fa-pen"></i></button>
<button id="boardDelete" class="btn badge secondary danger" title="削除"><i class="fa-regular fa-trash-can"></i></button>
</div>
<div class="pill" title="テーマ切替"><i class="fa-solid fa-circle-half-stroke"></i>
<label class="row-wrap"><input id="themeToggle" type="checkbox" /> ダーク</label>
</div>
<div class="pill" title="アクセントカラー">
<i class="fa-solid fa-palette"></i>
<input id="accentPicker" type="color" value="#2563eb" />
</div>
<div class="pill" title="列数"><i class="fa-solid fa-table-columns"></i>
<input id="colsRange" type="range" min="1" max="4" step="1" value="3"/>
<span id="colsVal" class="tiny"></span>
</div>
<div class="pill" title="カード高さ"><i class="fa-solid fa-up-down"></i>
<input id="heightRange" type="range" min="360" max="1200" step="40" value="640"/>
<span id="heightVal" class="tiny"></span>
</div>
<div class="pill" title="自動再読み込み">
<label class="row-wrap"><input id="autoRefreshToggle" type="checkbox"/> 自動</label>
<select id="refreshMinutes">
<option value="3">3分</option>
<option value="5" selected>5分</option>
<option value="10">10分</option>
<option value="30">30分</option>
</select>
</div>
<div class="pill help tiny">ショートカット: <span class="kbd">N</span> 追加 / <span class="kbd">R</span> 再描画 / <span class="kbd">G</span> グリッド- / <span class="kbd">H</span> 高さ-</div>
</div>
</header>
<main class="wrap grid">
<aside>
<div class="list">
<div class="section">
<h2>ソースを追加</h2>
<div class="toolbar row-wrap">
<select id="sourceType">
<option value="profile">プロフィール</option>
<option value="hashtag">ハッシュタグ</option>
<option value="search">検索キーワード</option>
<option value="list">リストURL</option>
</select>
<input id="sourceValue" class="input" type="text" placeholder="@username / #tag / キーワード / リストURL" />
<input id="sourceLabel" class="input" type="text" placeholder="表示名(任意)" />
<button id="addBtn" class="btn"><i class="fa-solid fa-plus"></i> 追加</button>
</div>
<div class="space"></div>
<label class="label">まとめて追加(改行/カンマ区切りOK)</label>
<textarea id="bulkArea" placeholder="@OpenAI, #UnrealEngine, Unity URP, https://twitter.com/i/lists/123...\n@EpicGames"></textarea>
<div class="row-wrap">
<button id="bulkAdd" class="btn secondary"><i class="fa-solid fa-download"></i> 取り込み</button>
<button id="bulkClear" class="btn ghost"><i class="fa-solid fa-eraser"></i> クリア</button>
</div>
<p class="help" style="margin-top:8px">形式は自動判定:<span class="kbd">@id</span> → プロフィール、<span class="kbd">#tag</span> → ハッシュタグ、<span class="kbd">twitter.com/i/lists</span> → リスト、それ以外は検索。</p>
<div class="space"></div>
<div class="row-wrap help tiny">クイック追加:</div>
<div class="row-wrap">
<button class="btn badge secondary quick" data-type="hashtag" data-val="#UnrealEngine">#UnrealEngine</button>
<button class="btn badge secondary quick" data-type="hashtag" data-val="#Unity3D">#Unity3D</button>
<button class="btn badge secondary quick" data-type="search" data-val="VRM OR \"Meta Quest\"">VR/Quest</button>
<button class="btn badge secondary quick" data-type="profile" data-val="@OpenAI">@OpenAI</button>
</div>
</div>
<div class="section">
<h2>保存・書き出し</h2>
<div class="toolbar row-wrap">
<button id="exportBtn" class="btn secondary"><i class="fa-solid fa-file-export"></i> JSON</button>
<label class="btn secondary" for="importFile"><i class="fa-solid fa-file-import"></i> JSON読込</label>
<input id="importFile" type="file" accept="application/json" hidden />
<button id="exportHtmlBtn" class="btn"><i class="fa-regular fa-file-code"></i> 単一HTML</button>
<button id="clearBtn" class="btn ghost danger"><i class="fa-regular fa-trash-can"></i> すべて削除</button>
</div>
<p class="help">単一HTML: いまのレイアウトと設定を埋め込んだ自立HTMLを生成します。</p>
</div>
<div class="section">
<h2>手動ランキング</h2>
<div class="toolbar row-wrap">
<input id="tweetUrl" class="input" type="text" placeholder="ツイートURLを貼り付け" />
<button id="addTweetBtn" class="btn"><i class="fa-brands fa-x-twitter"></i> 追加</button>
</div>
<label class="label">メモ(任意・次回以降も保持)</label>
<textarea id="tweetNote" placeholder="このツイートの要点やタグ(例: #UE5 #VRM)"></textarea>
<p class="help">※HTMLのみの制約で自動集計は不可。URLをカード化して手動で順序を決められます。</p>
</div>
<div class="section">
<h2>RSS生成(ランキング→RSS)</h2>
<div class="toolbar row-wrap">
<input id="rssTitle" class="input" type="text" placeholder="RSSタイトル(例: Xlogランキング)"/>
<button id="rssExport" class="btn secondary"><i class="fa-solid fa-rss"></i> RSSを書き出し</button>
</div>
<p class="help">ランキングに登録したツイートURLから簡易RSS(XML)を生成し、ファイルとして保存します。</p>
</div>
<div class="section">
<h2>ヘルプ</h2>
<div class="help">
・列の並べ替えはカードの <span class="kbd">⋯</span> アイコンをドラッグ。<br>
・<span class="kbd">R</span> で全カラムを再描画。<br>
・URLハッシュ <span class="kbd">#data=</span> に設定をBase64で埋め込んで共有可能(メニューから自動生成予定)。
</div>
</div>
</div>
</aside>
<section>
<div class="card" style="margin-bottom:16px">
<div class="head">
<div class="title"><i class="fa-solid fa-layer-group drag"></i> マイまとめ <span class="muted tiny" id="boardInfo"></span></div>
<div class="sources" id="activeChips"></div>
</div>
<div class="body" style="padding:14px">
<div id="columns" class="columns"></div>
</div>
</div>
<div class="card">
<div class="head">
<div class="title"><i class="fa-regular fa-star"></i> 手動ランキング</div>
<div class="help">ドラッグで順序変更/🗑で削除/✎でメモ編集</div>
</div>
<div class="body" style="padding:14px">
<div id="ranking" class="columns"></div>
</div>
</div>
<div class="footer">Xlog Pro v2 — HTML Only / Embedded Timelines. No API keys. <span class="muted">Made for you.</span></div>
</section>
</main>
<script async src="https://platform.twitter.com/widgets.js"></script>
<script>
// ========== 基本ユーティリティ ==========
const $ = (s, d=document)=>d.querySelector(s);
const $$ = (s, d=document)=>Array.from(d.querySelectorAll(s));
const defaultBoard = ()=>({sources:[], tweets:[]});
const defaultState = ()=>({
version:2,
dark:false,
accent:'#2563eb',
autoRefresh:false,
minutes:5,
columns:3,
cardHeight:640,
boards:{'Default': defaultBoard()},
activeBoard:'Default'
});
const store = {
key: 'xlog-pro-v2',
load(){
try{ return JSON.parse(localStorage.getItem(this.key)) || defaultState(); }
catch(e){ return defaultState(); }
},
save(v){ localStorage.setItem(this.key, JSON.stringify(v)); }
};
function migrate(s){
const base = defaultState();
if (!s || typeof s !== 'object') return base;
// v1互換(sources/tweets直下 → boards.Default)
if (s.sources || s.tweets){
base.boards.Default.sources = s.sources||[];
base.boards.Default.tweets = s.tweets||[];
}
// 既存キー上書き
for (const k of ['dark','accent','autoRefresh','minutes','columns','cardHeight','boards','activeBoard']){
if (k in s) base[k]=s[k];
}
return base;
}
// ハッシュ (#data=BASE64) から読み込み
function loadFromHash(){
const h = location.hash || '';
if (!h.startsWith('#data=')) return null;
try{
const b64 = decodeURIComponent(h.slice(6));
const json = atob(b64);
return JSON.parse(json);
}catch(e){ return null; }
}
let embedded = (typeof window.__XLOG_INITIAL_STATE__!== 'undefined') ? window.__XLOG_INITIAL_STATE__ : null;
if (!embedded){
const el = document.getElementById('xlog-init');
if (el) { try{ embedded = JSON.parse(el.textContent); }catch(_e){} }
}
let state = migrate( embedded || loadFromHash() || store.load() );
// ========== テーマ/アクセント/レイアウト適用 ==========
function applySkin(){
document.documentElement.setAttribute('data-theme', state.dark ? 'dark' : 'light');
document.documentElement.style.setProperty('--cols', state.columns);
document.documentElement.style.setProperty('--card-h', state.cardHeight+'px');
document.documentElement.style.setProperty('--accent', state.accent || '#2563eb');
$('#themeToggle').checked = !!state.dark;
$('#colsRange').value = String(state.columns);
$('#colsVal').textContent = state.columns+'列';
$('#heightRange').value = String(state.cardHeight);
$('#heightVal').textContent = state.cardHeight+'px';
$('#accentPicker').value = state.accent || '#2563eb';
}
// ========== X埋め込み ==========
function waitTwttr(){
return new Promise(res=>{
if (window.twttr && twttr.widgets) return res();
const timer = setInterval(()=>{ if(window.twttr && twttr.widgets){ clearInterval(timer); res(); } }, 200);
});
}
function timelineOptions(){
return {
height: state.cardHeight,
theme: state.dark ? 'dark' : 'light',
chrome: 'nofooter noborders transparent',
linkColor: getComputedStyle(document.documentElement).getPropertyValue('--link').trim() || '#1d9bf0'
};
}
async function createTimeline(el, src){
await waitTwttr();
const opts = timelineOptions();
const t = (src.type||'profile');
if (t==='profile'){
const screenName = src.value.replace(/^@/,'');
return twttr.widgets.createTimeline({ sourceType:'profile', screenName }, el, opts);
}
if (t==='list'){
return twttr.widgets.createTimeline({ sourceType:'url', url: src.value }, el, opts);
}
if (t==='hashtag'){
const tag = src.value.replace(/^#/,'');
const url = `https://twitter.com/hashtag/${encodeURIComponent(tag)}?f=live`;
return twttr.widgets.createTimeline({ sourceType:'url', url }, el, opts);
}
if (t==='search'){
const url = `https://twitter.com/search?q=${encodeURIComponent(src.value)}&f=live`;
return twttr.widgets.createTimeline({ sourceType:'url', url }, el, opts);
}
}
// ========== 現在ボードの参照 ==========
function board(){ return state.boards[state.activeBoard] || (state.boards[state.activeBoard]=defaultBoard()); }
// ========== 描画 ==========
function chipNode(src, idx){
const chip = document.createElement('span');
chip.className='chip';
const kind = {profile:'@',hashtag:'#',search:'検索:',list:'リスト'}[src.type] || '';
chip.innerHTML = `<b>${kind}</b> ${src.label || src.value} <a class="muted" href="${openUrl(src)}" target="_blank" title="Xで開く"><i class="fa-solid fa-arrow-up-right-from-square"></i></a> <button title="削除" data-del="${idx}" class="muted"><i class="fa-solid fa-xmark"></i></button>`;
chip.querySelector('button').onclick = ()=>{ board().sources.splice(idx,1); store.save(state); renderAll(); };
return chip;
}
function openUrl(src){
if (src.type==='profile') return `https://twitter.com/${src.value.replace(/^@/,'')}`;
if (src.type==='hashtag') return `https://twitter.com/hashtag/${src.value.replace(/^#/,'')}`;
if (src.type==='list') return src.value;
return `https://twitter.com/search?q=${encodeURIComponent(src.value)}&f=live`;
}
function columnCard(src, idx){
const card = document.createElement('div');
card.className='card';
card.draggable=true; card.dataset.idx=idx;
card.innerHTML = `
<div class="head">
<div class="title"><i class="fa-solid fa-grip-vertical drag"></i> ${src.label || prettyLabel(src)}</div>
<div class="toolbar">
<a class="btn ghost" href="${openUrl(src)}" target="_blank" title="Xで開く"><i class="fa-solid fa-arrow-up-right-from-square"></i></a>
<button class="btn ghost" title="再読み込み" data-refresh="${idx}"><i class="fa-solid fa-rotate"></i></button>
<button class="btn ghost danger" title="削除" data-remove="${idx}"><i class="fa-regular fa-trash-can"></i></button>
</div>
</div>
<div class="body"><div class="embed" style="min-height:120px"></div></div>`;
// DnD 並べ替え
card.addEventListener('dragstart', e=>{ e.dataTransfer.setData('text/plain', idx); card.style.opacity='0.6'; });
card.addEventListener('dragend', ()=>{ card.style.opacity='1'; });
card.addEventListener('dragover', e=>{ e.preventDefault(); card.style.outline='2px dashed var(--accent)'; });
card.addEventListener('dragleave', ()=>{ card.style.outline='none'; });
card.addEventListener('drop', e=>{
e.preventDefault(); card.style.outline='none';
const from = +e.dataTransfer.getData('text/plain');
const to = +card.dataset.idx;
if (from===to) return;
const arr = board().sources;
const [moved] = arr.splice(from,1);
arr.splice(to,0,moved);
store.save(state); renderAll();
});
// 操作
card.querySelector('[data-remove]')?.addEventListener('click', ()=>{ board().sources.splice(idx,1); store.save(state); renderAll(); });
card.querySelector('[data-refresh]')?.addEventListener('click', ()=>{ mountTimeline(card, src); });
// 初回描画
mountTimeline(card, src);
return card;
}
function mountTimeline(card, src){
const holder = card.querySelector('.embed');
holder.innerHTML = '<div style="padding:14px" class="muted">読み込み中…</div>';
createTimeline(holder, src).catch(()=>{
holder.innerHTML = '<div style="padding:14px" class="danger">読み込みに失敗しました。値を確認してください。</div>';
});
}
function prettyLabel(src){
if (src.type==='profile') return '@'+src.value.replace(/^@/,'');
if (src.type==='hashtag') return '#'+src.value.replace(/^#/,'');
if (src.type==='search') return '検索: '+src.value;
if (src.type==='list') return 'リスト';
return src.value;
}
function renderAll(){
// ボード情報
const info = $('#boardInfo');
const b = board();
info.textContent = `(${state.activeBoard}|${b.sources.length}列 / ${b.tweets.length}件)`;
// チップ
const chips = $('#activeChips'); chips.innerHTML='';
b.sources.forEach((s,i)=> chips.appendChild(chipNode(s,i)) );
// カラム
const col = $('#columns'); col.innerHTML='';
b.sources.forEach((s,i)=> col.appendChild(columnCard(s,i)) );
// ランキング
renderRanking();
}
// ========== ランキング ==========
function parseTweetId(url){ const m = (url||'').match(/status\/(\d{5,})/); return m? m[1] : null; }
function tweetUrlFromId(id){ return `https://twitter.com/i/web/status/${id}`; }
async function addTweet(url, note){
const id = parseTweetId(url);
if (!id) return alert('ツイートURLが正しくありません');
board().tweets.push({id, note: (note||'')});
store.save(state); renderRanking();
}
async function renderRanking(){
await waitTwttr();
const root = $('#ranking'); root.innerHTML='';
const arr = board().tweets;
arr.forEach((t, idx)=>{
const card = document.createElement('div'); card.className='card'; card.draggable=true; card.dataset.idx=idx;
card.innerHTML = `
<div class="head">
<div class="title"><i class="fa-solid fa-grip-vertical drag"></i> エントリ #${idx+1}</div>
<div class="toolbar">
<button class="btn ghost" data-edit="${idx}" title="メモ編集"><i class="fa-regular fa-pen-to-square"></i></button>
<a class="btn ghost" href="${tweetUrlFromId(t.id)}" target="_blank" title="Xで開く"><i class="fa-solid fa-arrow-up-right-from-square"></i></a>
<button class="btn ghost danger" title="削除" data-del-rank="${idx}"><i class="fa-regular fa-trash-can"></i></button>
</div>
</div>
<div class="body">
<div class="embed"></div>
<div style="padding:10px 14px;border-top:1px solid var(--border)" class="tiny"><span class="muted">メモ:</span> <span class="note">${escapeHtml(t.note||'')}</span></div>
</div>`;
// イベント
card.querySelector('[data-del-rank]')?.addEventListener('click', ()=>{ arr.splice(idx,1); store.save(state); renderRanking(); });
card.querySelector('[data-edit]')?.addEventListener('click', ()=>{
const newNote = prompt('メモを編集', t.note||'');
if (newNote!==null){ t.note = newNote; store.save(state); renderRanking(); }
});
// DnD 並べ替え
card.addEventListener('dragstart', e=>{ e.dataTransfer.setData('text/plain', 'rank:'+idx); card.style.opacity='0.6'; });
card.addEventListener('dragend', ()=>{ card.style.opacity='1'; });
card.addEventListener('dragover', e=>{ e.preventDefault(); card.style.outline='2px dashed var(--accent)'; });
card.addEventListener('dragleave', ()=>{ card.style.outline='none'; });
card.addEventListener('drop', e=>{
e.preventDefault(); card.style.outline='none';
const data = e.dataTransfer.getData('text/plain'); if (!data.startsWith('rank:')) return;
const from = +data.split(':')[1]; const to = +card.dataset.idx;
const [moved] = arr.splice(from,1); arr.splice(to,0,moved);
store.save(state); renderRanking();
});
const holder = card.querySelector('.embed');
twttr.widgets.createTweet(t.id, holder, { theme: state.dark ? 'dark' : 'light' });
root.appendChild(card);
});
}
function escapeHtml(s){ return (s||'').replace(/[&<>"']/g, m=> ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[m])); }
// ========== 自動再描画 ==========
let refreshTimer = null;
function applyAutoRefresh(){
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer=null; }
if (state.autoRefresh){
const ms = Math.max(1, +state.minutes) * 60 * 1000;
refreshTimer = setInterval(()=>{
$$('#columns .card').forEach((card, i)=>{
const src = board().sources[i]; if (src) mountTimeline(card, src);
});
}, ms);
}
}
// ========== ボード管理 ==========
function refreshBoardSelect(){
const sel = $('#boardSelect'); sel.innerHTML='';
Object.keys(state.boards).forEach(name=>{
const opt = document.createElement('option'); opt.value=name; opt.textContent=name; sel.appendChild(opt);
});
sel.value = state.activeBoard;
}
function addBoard(name){
if (!name) return;
if (state.boards[name]) return alert('同名のボードが存在します');
state.boards[name] = defaultBoard(); state.activeBoard = name; store.save(state);
refreshBoardSelect(); renderAll();
}
function renameBoard(newName){
if (!newName) return;
if (state.boards[newName]) return alert('同名のボードが存在します');
const old = state.activeBoard;
state.boards[newName] = state.boards[old];
delete state.boards[old];
state.activeBoard = newName; store.save(state);
refreshBoardSelect(); renderAll();
}
function deleteBoard(){
const names = Object.keys(state.boards);
if (names.length<=1) return alert('最後のボードは削除できません');
if (!confirm(`ボード「${state.activeBoard}」を削除しますか?`)) return;
delete state.boards[state.activeBoard];
state.activeBoard = Object.keys(state.boards)[0];
store.save(state); refreshBoardSelect(); renderAll();
}
// ========== 共有(URLハッシュ生成) ==========
function exportHashUrl(){
const cloned = JSON.parse(JSON.stringify(state));
const json = JSON.stringify(cloned);
const b64 = btoa(json);
const url = location.origin + location.pathname + '#data=' + encodeURIComponent(b64);
return url;
}
// ========== 単一HTML出力 ==========
function download(filename, text){
const blob = new Blob([text], {type:'text/html'});
const a = document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=filename; a.click(); URL.revokeObjectURL(a.href);
}
function exportSingleHtml(){
// 現在のHTMLに初期状態スクリプトを差し込む
let html = document.documentElement.outerHTML;
const idx = html.indexOf('<head>');
const inject = `<head>\n <script>window.__XLOG_INITIAL_STATE__=${JSON.stringify(state)}<\/script>`;
if (idx>=0){ html = html.replace('<head>', inject); }
download('xlog-pro.html', '<!DOCTYPE html>\n' + html);
}
// ========== RSS生成 ==========
function exportRss(){
const title = $('#rssTitle').value.trim() || 'Xlog Ranking';
const items = board().tweets.map(t=>({
title: (t.note||'Tweet '+t.id).replace(/[\r\n]+/g,' ').slice(0,120),
link: tweetUrlFromId(t.id),
guid: t.id,
description: escapeHtml(t.note||''),
pubDate: new Date().toUTCString()
}));
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0"><channel>\n<title>${escapeXml(title)}</title>\n<link>${escapeXml(location.href)}</link>\n<description>Generated by Xlog Pro</description>\n${items.map(i=>`<item><title>${escapeXml(i.title)}</title><link>${escapeXml(i.link)}</link><guid isPermaLink=\"false\">${escapeXml(i.guid)}</guid><description>${escapeXml(i.description)}</description><pubDate>${i.pubDate}</pubDate></item>`).join('')}\n</channel></rss>`;
const blob = new Blob([xml], {type:'application/rss+xml'});
const a = document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='xlog-ranking.xml'; a.click(); URL.revokeObjectURL(a.href);
}
function escapeXml(s){ return (s||'').replace(/[<>&\"']/g, m=> ({'<':'<','>':'>','&':'&','\"':'"','\'':'''}[m])); }
// ========== 入力ヘルパ ==========
function detectType(v){
if (/^@/.test(v)) return 'profile';
if (/^#/.test(v)) return 'hashtag';
if (/twitter\.com\/i\/lists\//.test(v)) return 'list';
return 'search';
}
function addSource(type, value, label){
const src = {type, value:value.trim(), label:(label||'').trim()};
board().sources.push(src); store.save(state); renderAll();
}
function bulkAddFromText(txt){
const parts = txt.split(/[\n,]+/).map(s=>s.trim()).filter(Boolean);
let count = 0;
for (const p of parts){ addSource(detectType(p), p, ''); count++; }
return count;
}
// ========== キーイベント ==========
function setupShortcuts(){
window.addEventListener('keydown', (e)=>{
if (['INPUT','TEXTAREA','SELECT'].includes(document.activeElement.tagName)) return;
if (e.key==='n' || e.key==='N'){ $('#sourceValue').focus(); }
if (e.key==='r' || e.key==='R'){ renderAll(); }
if (e.key==='g' || e.key==='G'){ state.columns=Math.max(1,state.columns-1); applySkin(); store.save(state); }
if (e.key==='h' || e.key==='H'){ state.cardHeight=Math.max(360,state.cardHeight-40); applySkin(); store.save(state); renderAll(); }
});
}
// ========== 設定と起動 ==========
window.addEventListener('DOMContentLoaded', ()=>{
// スキン
applySkin();
// ボード選択
refreshBoardSelect();
$('#boardSelect').addEventListener('change', (e)=>{ state.activeBoard = e.target.value; store.save(state); renderAll(); });
$('#boardNew').addEventListener('click', ()=>{ const name = prompt('新しいボード名','Board '+(Object.keys(state.boards).length+1)); addBoard(name); });
$('#boardRename').addEventListener('click', ()=>{ const name = prompt('新しい名前', state.activeBoard); if (name) renameBoard(name); });
$('#boardDelete').addEventListener('click', deleteBoard);
// テーマ/アクセント/レイアウト
$('#themeToggle').addEventListener('change', e=>{ state.dark = e.target.checked; store.save(state); applySkin(); renderAll(); });
$('#accentPicker').addEventListener('input', e=>{ state.accent = e.target.value; store.save(state); applySkin(); });
$('#colsRange').addEventListener('input', e=>{ state.columns = +e.target.value; store.save(state); applySkin(); });
$('#heightRange').addEventListener('input', e=>{ state.cardHeight = +e.target.value; store.save(state); applySkin(); renderAll(); });
// ソース追加
$('#addBtn').addEventListener('click', ()=>{
const type = $('#sourceType').value;
const val = $('#sourceValue').value.trim();
const label = $('#sourceLabel').value.trim();
if (!val) return alert('値を入力してください');
addSource(type, val, label);
$('#sourceValue').value=''; $('#sourceLabel').value='';
});
$$('.quick').forEach(btn=> btn.addEventListener('click', ()=> addSource(btn.dataset.type, btn.dataset.val, '')) );
// まとめて追加
$('#bulkAdd').addEventListener('click', ()=>{ const n = bulkAddFromText($('#bulkArea').value); alert(n+'件追加しました'); $('#bulkArea').value=''; });
$('#bulkClear').addEventListener('click', ()=> $('#bulkArea').value='' );
// ランキング
$('#addTweetBtn').addEventListener('click', ()=>{
const url = $('#tweetUrl').value.trim();
const note = $('#tweetNote').value.trim();
if (!url) return;
addTweet(url, note); $('#tweetUrl').value=''; $('#tweetNote').value='';
});
// 設定
$('#autoRefreshToggle').checked = !!state.autoRefresh;
$('#refreshMinutes').value = String(state.minutes||5);
$('#autoRefreshToggle').addEventListener('change', e=>{ state.autoRefresh = e.target.checked; store.save(state); applyAutoRefresh(); });
$('#refreshMinutes').addEventListener('change', e=>{ state.minutes = +e.target.value; store.save(state); applyAutoRefresh(); });
// 書き出し/読み込み
$('#exportBtn').addEventListener('click', ()=>{
const blob = new Blob([JSON.stringify(state,null,2)], {type:'application/json'});
const a = document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='xlog-pro-config.json'; a.click(); URL.revokeObjectURL(a.href);
});
$('#importFile').addEventListener('change', e=>{
const file = e.target.files?.[0]; if (!file) return;
const fr = new FileReader();
fr.onload = () => {
try{ const obj = JSON.parse(fr.result); state = migrate(obj); store.save(state); applySkin(); refreshBoardSelect(); renderAll(); applyAutoRefresh(); }
catch(err){ alert('JSONの読み込みに失敗しました'); }
};
fr.readAsText(file);
});
$('#exportHtmlBtn').addEventListener('click', exportSingleHtml);
$('#clearBtn').addEventListener('click', ()=>{
if (!confirm('現在のボードのソースとランキングを削除しますか?')) return;
const b = board(); b.sources = []; b.tweets=[]; store.save(state); renderAll();
});
// RSS
$('#rssExport').addEventListener('click', exportRss);
// 初期描画
renderAll();
applyAutoRefresh();
setupShortcuts();
});
</script>
</body>
</html>
投稿者: chosuke
BranchBoard.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>GBranchBoard</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap & FontAwesome -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
<style>
body {
background: #f6f8fa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.repo-header { background: #fff; padding: 20px; border: 1px solid #ddd; margin-bottom: 10px; }
.nav-tabs .nav-link.active { border-color: #ddd #ddd #fff; background: #fff; }
.file-list li { border-bottom: 1px solid #eee; padding: 8px 0; display: flex; align-items: center; }
.file-list i { margin-right: 10px; }
.readme-box, .issues-box, .commits-box { background: #fff; padding: 20px; border: 1px solid #ddd; margin-top: 10px; }
#editor { height: 400px; width: 100%; border: 1px solid #ccc; }
</style>
</head>
<body>
<!-- ヘッダー -->
<nav class="navbar navbar-dark bg-dark px-3">
<span class="text-white"><i class="fas fa-user-circle"></i> owner</span>
</nav>
<div class="container mt-3">
<!-- リポジトリヘッダー -->
<div class="repo-header">
<h3><i class="fas fa-book"></i> owner / <strong>SampleRepo</strong></h3>
<p class="text-muted">最終更新: 2025年5月26日</p>
<button class="btn btn-sm btn-outline-secondary"><i class="fas fa-star"></i> Star</button>
<button class="btn btn-sm btn-outline-secondary"><i class="fas fa-code-branch"></i> Fork</button>
</div>
<!-- タブ -->
<ul class="nav nav-tabs" id="repoTabs">
<li class="nav-item"><a class="nav-link active" href="#" onclick="switchTab('code')"><i class="fas fa-code"></i> Code</a></li>
<li class="nav-item"><a class="nav-link" href="#" onclick="switchTab('issues')"><i class="fas fa-exclamation-circle"></i> Issues</a></li>
<li class="nav-item"><a class="nav-link" href="#" onclick="switchTab('commits')"><i class="fas fa-history"></i> Commits</a></li>
</ul>
<!-- Codeタブ -->
<div id="tab-code" class="tab-content">
<ul class="file-list list-unstyled bg-white p-3 border">
<li><i class="fas fa-folder"></i> src/</li>
<li><i class="fas fa-file-code"></i> index.js</li>
<li><i class="fas fa-file-alt"></i> README.md</li>
<li><i class="fas fa-file-alt"></i> LICENSE</li>
</ul>
<div class="readme-box">
<h4><i class="fas fa-book-open"></i> README.md</h4>
<hr>
<div id="readme-content"></div>
</div>
<div class="mt-4">
<h5><i class="fas fa-code"></i> コード編集 (Monaco Editor)</h5>
<div id="editor"></div>
<button id="saveCode" class="btn btn-success mt-2"><i class="fas fa-save"></i> 保存</button>
<button id="themeToggle" class="btn btn-secondary mt-2"><i class="fas fa-adjust"></i> テーマ切替</button>
</div>
</div>
<!-- Issuesタブ -->
<div id="tab-issues" class="tab-content" style="display:none;">
<div class="issues-box">
<h4><i class="fas fa-exclamation-circle"></i> Open Issues</h4>
<ul class="list-group">
<li class="list-group-item">
<strong>#1</strong> READMEの翻訳が必要です<br>
<small class="text-muted">posted by owner - 1日前</small>
</li>
<li class="list-group-item">
<strong>#2</strong> index.jsにテストコードを追加してください<br>
<small class="text-muted">posted by owner - 2日前</small>
</li>
</ul>
</div>
</div>
<!-- Commitsタブ -->
<div id="tab-commits" class="tab-content" style="display:none;">
<div class="commits-box">
<h4><i class="fas fa-history"></i> Commits</h4>
<ul class="list-group">
<li class="list-group-item">
<strong>Initial commit</strong> - 2025-05-25<br>
<small class="text-muted">by owner</small>
</li>
<li class="list-group-item">
<strong>README updated</strong> - 2025-05-26<br>
<small class="text-muted">by owner</small>
</li>
</ul>
</div>
</div>
</div>
<!-- ライブラリ -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
<!-- タブ切替・Markdown表示・Monaco起動 -->
<script>
const markdown = `
# SampleRepo
## 特徴
- Monaco Editorでコード編集
- Markdown表示(README)
- ダークモード対応
- Issue・コミットのUI
## 技術
- HTML/CSS/JS
- Bootstrap5
- Monaco Editor
- Marked.js
`;
document.getElementById('readme-content').innerHTML = marked.parse(markdown);
function switchTab(tab) {
['code', 'issues', 'commits'].forEach(id => {
document.getElementById('tab-' + id).style.display = (id === tab) ? 'block' : 'none';
});
document.querySelectorAll('#repoTabs .nav-link').forEach(link => link.classList.remove('active'));
document.querySelector(`#repoTabs .nav-link[onclick*="${tab}"]`).classList.add('active');
}
// Monaco起動
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' }});
require(['vs/editor/editor.main'], function () {
window.editor = monaco.editor.create(document.getElementById('editor'), {
value: localStorage.getItem('savedCode') || `function hello() {\n console.log("Hello from Monaco Editor!");\n}`,
language: 'javascript',
theme: 'vs-light',
automaticLayout: true
});
document.getElementById('saveCode').onclick = function () {
const code = editor.getValue();
localStorage.setItem('savedCode', code);
alert('保存しました!');
};
document.getElementById('themeToggle').onclick = function () {
const theme = monaco.editor.getTheme() === 'vs-dark' ? 'vs-light' : 'vs-dark';
monaco.editor.setTheme(theme);
};
});
</script>
</body>
</html>
Tsumugi.html(SNS)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Tsumugi</title>
<!-- Favicon -->
<link rel="shortcut icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%23667eea' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/></svg>"/>
<!-- Tailwind CSS v2 -->
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"/>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css"/>
<style>
:root {
--grad-a: #667eea;
--grad-b: #764ba2;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, var(--grad-a) 0%, var(--grad-b) 100%);
min-height: 100vh;
}
.glass-effect {
background: rgba(255,255,255,0.25);
backdrop-filter: blur(10px);
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.18);
}
.card-hover { transition: all 0.25s ease; }
.card-hover:hover { transform: translateY(-3px); box-shadow: 0 20px 30px -12px rgba(0,0,0,0.25); }
.gradient-text {
background: linear-gradient(45deg, var(--grad-a), var(--grad-b));
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
.timeline-post {
background: white;
border-radius: 16px;
box-shadow: 0 10px 20px -12px rgba(0,0,0,0.2);
transition: all 0.25s ease;
border-left: 4px solid var(--grad-a);
}
.timeline-post:hover { transform: translateX(4px); }
.profile-avatar {
width: 100px; height: 100px; border-radius: 50%;
object-fit: cover; border: 4px solid white;
box-shadow: 0 10px 24px rgba(0,0,0,0.15);
}
.btn-primary {
background: linear-gradient(45deg, var(--grad-a), var(--grad-b));
border: none; transition: all 0.2s ease;
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 10px 18px rgba(0,0,0,0.18); }
.section-divider {
height: 3px; background: linear-gradient(90deg, var(--grad-a), var(--grad-b));
border-radius: 2px; margin: 2rem 0;
}
.username-badge {
background: linear-gradient(45deg, var(--grad-a), var(--grad-b));
color: white; padding: 0.2rem 0.5rem; border-radius: 1rem;
font-size: 0.75rem; font-weight: 600; display: inline-block; margin-left: 0.5rem;
}
.share-menu { position: absolute; z-index: 50; min-width: 180px; right: 0; top: 110%; background: white; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.13); }
.share-menu button { width: 100%; text-align: left; padding: 10px 20px; border: none; background: none; cursor: pointer; font-size: 0.95rem; }
.share-menu button:hover { background: #f0f4ff; }
.status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
.status-active { background-color: #10b981; animation: pulse 2s infinite; }
.status-inactive { background-color: #6b7280; }
.log-container {
max-height: 180px; overflow-y: auto; background: rgba(255,255,255,0.12);
border-radius: 10px; padding: 10px; margin-top: 10px; font-size: 0.8rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.error-message { color: #ef4444; background: rgba(239, 68, 68, 0.08); padding: 8px; border-radius: 8px; margin: 5px 0; }
.success-message { color: #10b981; background: rgba(16, 185, 129, 0.08); padding: 8px; border-radius: 8px; margin: 5px 0; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
.dark .glass-effect { background: rgba(0,0,0,0.30); border: 1px solid rgba(255,255,255,0.12); }
.dark .timeline-post { background: #1f2937; color: #f9fafb; border-left-color: #4f46e5; }
.dark .share-menu { background: #111827; color: #e5e7eb; }
.dark .success-message { background: rgba(16,185,129,0.12); }
.dark .error-message { background: rgba(239,68,68,0.12); }
@media print {
body { background: white !important; -webkit-print-color-adjust: exact; }
.glass-effect { background: white !important; backdrop-filter: none !important; border: 1px solid #e5e7eb !important; }
}
</style>
</head>
<body>
<!-- ログイン/登録モーダル -->
<div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" style="display:none">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-sm p-6">
<h2 class="text-2xl font-bold mb-4 text-center" id="auth-title">ログイン</h2>
<div id="auth-error" class="error-message mb-2" style="display:none"></div>
<form id="auth-form" autocomplete="off">
<div class="mb-3">
<label class="block mb-1 text-sm font-semibold">メールアドレス</label>
<input type="email" id="auth-email" class="w-full border rounded px-3 py-2" required>
</div>
<div class="mb-3">
<label class="block mb-1 text-sm font-semibold">パスワード</label>
<input type="password" id="auth-password" class="w-full border rounded px-3 py-2" required>
</div>
<button type="submit" class="btn-primary w-full py-2 rounded-lg text-white font-semibold">ログイン</button>
</form>
<div class="mt-4 text-center">
<button id="toggle-auth-mode" class="text-blue-600 underline text-sm">新規登録はこちら</button>
</div>
</div>
</div>
<!-- ヘッダー -->
<header class="glass-effect mx-4 mt-4 p-6">
<div class="text-center">
<h1 class="text-4xl font-bold text-white mb-2">
<i class="fas fa-comments mr-3"></i>Tsumugi
<span class="text-sm opacity-80 ml-2"></span>
</h1>
<p class="text-white text-lg opacity-90">次世代ソーシャルネットワーク</p>
<div class="mt-4 flex justify-center items-center space-x-4">
<div class="flex items-center">
<img id="header-profile-icon" class="profile-avatar" src="https://via.placeholder.com/100" alt="プロフィール">
<div class="ml-3 text-white text-left">
<div class="font-semibold text-lg" id="header-username">未設定</div>
<div class="text-sm opacity-75" id="header-user-email"></div>
</div>
</div>
<button onclick="toggleDarkMode()" class="btn-primary px-4 py-2 rounded-full text-white">
<i class="fas fa-moon mr-2"></i>ダークモード
</button>
<button onclick="showSystemStatus()" class="btn-primary px-4 py-2 rounded-full text-white">
<i class="fas fa-info-circle mr-2"></i>ステータス
</button>
<button id="logout-btn" onclick="logout()" class="btn-primary px-4 py-2 rounded-full text-white" style="display:none">
<i class="fas fa-sign-out-alt mr-2"></i>ログアウト
</button>
</div>
</div>
</header>
<!-- メイン -->
<div class="max-w-6xl mx-auto px-4 py-6" id="main-content" style="display:none">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 左カラム:プロフィール/BOT/RSS -->
<div class="lg:col-span-1 space-y-6">
<!-- プロフィール -->
<div class="glass-effect p-6 card-hover">
<h3 class="text-2xl font-bold gradient-text mb-4"><i class="fas fa-user-circle mr-2"></i>プロフィール</h3>
<div class="text-center mb-6">
<img id="profile-icon" class="profile-avatar mx-auto mb-4" src="https://via.placeholder.com/100" alt="プロフィール">
<input type="file" id="profile-upload" accept="image/*" onchange="uploadProfileIcon(event)" class="hidden">
<button onclick="document.getElementById('profile-upload').click()" class="btn-primary px-4 py-2 rounded-full text-white">
<i class="fas fa-camera mr-2"></i>画像変更
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-white font-semibold mb-2">ユーザー名</label>
<input type="text" id="username" class="w-full p-3 border rounded-lg bg-white bg-opacity-90" placeholder="ユーザー名を入力してください" maxlength="20">
</div>
<div>
<label class="block text-white font-semibold mb-2">自己紹介</label>
<textarea id="self-intro" class="w-full p-3 border rounded-lg bg-white bg-opacity-90" rows="4" placeholder="自己紹介を入力してください"></textarea>
</div>
<button onclick="saveProfile()" class="btn-primary w-full py-2 rounded-lg text-white">
<i class="fas fa-save mr-2"></i>プロフィール保存
</button>
<div class="p-3 bg-white bg-opacity-80 rounded-lg">
<h5 class="font-semibold text-gray-800 mb-2">プレビュー:</h5>
<div class="text-gray-700">
<div class="font-semibold mb-1" id="username-preview">未設定</div>
<div id="self-intro-preview" class="text-sm whitespace-pre-line min-h-8">まだ自己紹介がありません</div>
</div>
</div>
</div>
</div>
<!-- RSS自動投稿機能(全体共有 + 個別ON/OFF) -->
<div class="glass-effect p-6 card-hover">
<h3 class="text-xl font-bold text-white mb-4">
<i class="fas fa-rss mr-2"></i>RSS自動投稿(全体共有)
<span class="status-indicator" id="rss-status"></span>
<span id="rss-status-text" class="text-xs opacity-75">停止中</span>
</h3>
<div>
<input id="rss-url" type="text" class="w-full p-2 border rounded-lg bg-white bg-opacity-90 mb-2" placeholder="RSSフィードURLを入力">
<button onclick="addRssFeed()" class="btn-primary w-full py-2 rounded-lg text-white mb-2">
<i class="fas fa-plus mr-2"></i>追加
</button>
<div id="rss-list" class="mb-3"></div>
<div class="flex items-center space-x-2 mb-2">
<input type="number" id="rss-interval" class="w-1/2 p-2 border rounded-lg bg-white bg-opacity-90" min="10" max="3600" value="300" placeholder="間隔(秒)">
<button onclick="setRssInterval()" class="btn-primary flex-1 py-2 rounded-lg text-white">
<i class="fas fa-clock mr-2"></i>間隔設定
</button>
</div>
<div class="flex items-center space-x-2 mb-2">
<button onclick="fetchRssNow()" class="btn-primary flex-1 py-2 rounded-lg text-white">
<i class="fas fa-sync mr-2"></i>今すぐ取得
</button>
<button onclick="stopRssAuto()" class="bg-red-500 hover:bg-red-600 flex-1 py-2 rounded-lg text-white">
<i class="fas fa-stop mr-2"></i>自動停止
</button>
</div>
<div class="flex items-center space-x-2">
<button onclick="setAllRssEnabled(true)" class="btn-primary flex-1 py-2 rounded-lg text-white text-sm">
<i class="fas fa-toggle-on mr-1"></i>すべてON
</button>
<button onclick="setAllRssEnabled(false)" class="bg-gray-500 hover:bg-gray-600 flex-1 py-2 rounded-lg text-white text-sm">
<i class="fas fa-toggle-off mr-1"></i>すべてOFF
</button>
</div>
<div id="rss-log" class="log-container text-white text-xs mt-3"></div>
</div>
</div>
<!-- BOT機能 -->
<div class="glass-effect p-6 card-hover">
<h3 class="text-xl font-bold text-white mb-4">
<i class="fas fa-robot mr-2"></i>BOT機能
<span class="status-indicator" id="bot-status"></span>
<span id="bot-status-text" class="text-xs opacity-75">停止中</span>
</h3>
<div class="space-y-4">
<div>
<textarea id="botContent" class="w-full p-3 border rounded-lg bg-white bg-opacity-90" rows="3" placeholder="BOT投稿内容"></textarea>
<button onclick="postBotMessage()" class="btn-primary w-full mt-2 py-2 rounded-lg text-white">
<i class="fas fa-robot mr-2"></i>BOT投稿
</button>
</div>
<div>
<input type="number" id="botIntervalSec" class="w-full p-2 border rounded-lg bg-white bg-opacity-90" placeholder="マルコフ自動投稿間隔(秒)" min="10" max="3600" value="60">
<div class="flex space-x-2 mt-2">
<button onclick="postMarkovBot()" class="btn-primary flex-1 py-2 rounded-lg text-white">
<i class="fas fa-dice mr-2"></i>マルコフ生成
</button>
<button onclick="startBotAutoPost()" class="btn-primary flex-1 py-2 rounded-lg text-white">
<i class="fas fa-play mr-2"></i>自動開始
</button>
<button onclick="stopBotAutoPost()" class="bg-red-500 hover:bg-red-600 flex-1 py-2 rounded-lg text-white">
<i class="fas fa-stop mr-2"></i>停止
</button>
</div>
</div>
<div class="text-white text-xs opacity-75">
<i class="fas fa-info-circle mr-1"></i>
マルコフ連鎖では過去の投稿からランダムな文章を生成します(FEED本文は省略し、タイトルのみを学習対象にしません)。
</div>
<div id="bot-log" class="log-container text-white text-xs"></div>
</div>
</div>
</div>
<!-- 右カラム:投稿&タイムライン -->
<div class="lg:col-span-2 space-y-6">
<!-- 新規投稿 -->
<div class="glass-effect p-6 card-hover">
<h3 class="text-2xl font-bold gradient-text mb-4"><i class="fas fa-edit mr-2"></i>新規投稿</h3>
<div>
<textarea id="postContent" class="w-full p-4 border rounded-lg bg-white bg-opacity-90" rows="4" placeholder="今何を考えていますか?" maxlength="500"></textarea>
<div class="mt-4 flex justify-between items-center">
<div class="text-white text-sm opacity-90">
<i class="fas fa-info-circle mr-1"></i>
あなたの思いを共有しましょう
<span id="char-count" class="ml-2">(0/500)</span>
</div>
<button onclick="createUserPost()" class="btn-primary px-6 py-3 rounded-lg text-white font-semibold">
<i class="fas fa-paper-plane mr-2"></i>投稿する
</button>
</div>
</div>
</div>
<div class="section-divider"></div>
<!-- タイムライン -->
<div class="glass-effect p-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-2xl font-bold text-white">
<i class="fas fa-stream mr-2"></i>タイムライン
<span id="post-count" class="text-sm font-normal opacity-75 ml-2">(0件の投稿)</span>
</h3>
<div class="flex space-x-2">
<button onclick="clearAllPosts()" class="bg-red-500 hover:bg-red-600 px-3 py-1 rounded text-white text-sm">
<i class="fas fa-trash mr-1"></i>全削除
</button>
<button onclick="exportData()" class="bg-green-500 hover:bg-green-600 px-3 py-1 rounded text-white text-sm">
<i class="fas fa-download mr-1"></i>エクスポート
</button>
</div>
</div>
<div id="timeline" class="space-y-4"></div>
<div id="empty-timeline" class="text-center py-12 text-white opacity-80">
<i class="fas fa-comments text-4xl mb-4"></i>
<p class="text-lg">まだ投稿がありません</p>
<p class="text-sm">最初の投稿をして、タイムラインを始めましょう!</p>
</div>
</div>
</div>
</div>
</div>
<footer class="glass-effect mx-4 mb-4 p-4 text-center">
<p class="text-white opacity-80">
<i class="fas fa-copyright mr-2"></i>2025 Verse – 次世代ソーシャルネットワーク v2.3
<span class="ml-4"><i class="fas fa-rss mr-1"></i>共有RSS / 個別ON/OFF対応(安全なフィード例)</span>
</p>
</footer>
<script>
// ==== 初期RSS(安全な例のみ。成人向け/不適切なサイトは除外) ====
const PRESET_RSS = [
"http://2ch-2.net/rss/all.xml",
"http://2ch-ranking.net/rss/livemarket1.rdf",
"http://2ch-ranking.net/rss/livemarket2.rdf",
"http://kabumatome.doorblog.jp/index.rdf",
"http://momoniji.com/feed",
"http://oekakigakusyuu.blog97.fc2.com/?xml",
"http://otanews.livedoor.biz/atom.xml",
"http://otanews.livedoor.biz/index.rdf",
"http://news4vip.livedoor.biz/index.rdf",
"http://news.kakaku.com/prdnews/rss.asp",
"http://www.jma-net.go.jp/rss/jma.rss",
"http://rss.asahi.com/rss/asahi/newsheadlines.rdf",
"https://uploadvr.com/feed/",
"http://www.atmarkit.co.jp/rss/rss2dc.xml",
"http://liginc.co.jp/feed",
"http://liginc.co.jp/feed/",
"http://blog.livedoor.jp/shachiani/index.rdf",
"http://manga.lemon-s.com/atom.xml",
"http://b.hatena.ne.jp/search/text?safe=on&q=%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3&users=500&mode=rss",
"http://creator-life.net/feed/",
"http://feedblog.ameba.jp/rss/ameblo/ca-1pixel/rss20.xml",
"http://rssblog.ameba.jp/ca-1pixel/rss20.xml",
"http://weekly.ascii.jp/cate/1/rss.xml",
"http://blog.livedoor.jp/coleblog/atom.xml",
"http://2chantena.antenam.biz/rss1s.rss",
"http://www.4gamer.net/rss/index.xml",
"http://www.4gamer.net/rss/news_topics.xml",
"http://nyan.eggtree.net/feed.xml",
"http://nunnnunn.hatenablog.com/rss",
"http://www.nikkansports.com/general/atom.xml",
"http://feeds.afpbb.com/rss/afpbb/access_ranking",
"http://akiba-pc.watch.impress.co.jp/cda/rss/akiba-pc.rdf",
"https://area.autodesk.jp/rss.xml",
"http://av.watch.impress.co.jp/sublink/av.rdf",
"http://rss.allabout.co.jp/aa/latest/ch/netpc/",
"http://www.ar-ch.org/atom.xml",
"http://feeds.arstechnica.com/arstechnica/BAaf",
"https://feeds.feedburner.com/awwwards-sites-of-the-day",
"http://news.bbc.co.uk/rss/newsonline_uk_edition/front_page/rss091.xml",
"http://www.criteo.com/blog/rss/",
"https://blueskyweb.xyz/rss.xml",
"http://boingboing.net/rss.xml",
"http://www.cc2.co.jp/blog/?feed=rss2",
"http://cgarena.com/cgarena.xml",
"http://cgtracking.net/feed",
"http://japan.cnet.com/rss/index.rdf",
"http://newclassic.jp/feed",
"https://www.cssmania.com/feed/",
"http://ceron.jp/top/?type=rss",
"http://blog.btrax.com/jp/comments/feed/",
"http://2ch-ranking.net/rss/wildplus.rdf",
"http://design-spice.com/feed/",
"http://dev.classmethod.jp/feed/",
"http://ishida-a-coicoi.blog.so-net.ne.jp/atom.xml",
"http://feeds.feedburner.com/ura-akiba?format=xml",
"http://game.watch.impress.co.jp/sublink/game.rdf",
"http://gigazine.co.jp/feed/",
"http://labs.gree.jp/blog/comments/feed/",
"http://www.gamespark.jp/rss/index.rdf",
"http://www.gamebusiness.jp/rss/index.rdf",
"http://www.gamebusiness.jp/rss/rss.php",
"http://hackread.com/feed/",
"https://io3000.com/feed/",
"http://www.inside-games.jp/rss/index.rdf",
"http://blog.livedoor.jp/itsoku/index.rdf",
"http://rss.itmedia.co.jp/rss/1.0/news_bursts.xml",
"http://octoba.net/feed",
"http://www.ota-suke.jp/index.xml",
"http://blog.livedoor.jp/kaigai_no/index.rdf",
"https://land-book.com/feed/",
"http://2ch.logpo.jp/1hour.xml",
"http://menthas.com/javascript/rss",
"http://www.nhk.or.jp/rss/news/cat0.xml",
"http://ozpa-h4.com/feed/",
"https://www.youtube.com/feeds/videos.xml?channel_id=UC1DCedRgGHBdm81E1llLhOQ",
"http://rass.blog43.fc2.com/?xml",
"http://stackoverflow.com/feeds",
"http://www.slideshare.net/rss/latest",
"http://www.jp.square-enix.com/whatsnew2/whatsnew.rdf",
"http://www.ituore.com/feed",
"http://synodos.jp/comments/feed",
"http://www.shinkigensha.co.jp/feed/",
"http://e-shuushuu.net/index.rss",
"http://slashdot.org/index.rss",
"http://feeds.feedburner.com/TheHackersNews?format=xml",
"http://googleblog.blogspot.com/atom.xml",
"http://www.theregister.co.uk/tonys/slashdot.rdf",
"http://thinkit.co.jp/rss.xml",
"http://blog.livedoor.jp/news23vip/atom.xml",
"http://blog.livedoor.jp/news23vip/index.rdf",
"http://www.webcreatorbox.com/feed/",
"http://web-d.navigater.info/atom.xml",
"http://2ch-c.net/?xml_all",
"http://smhn.info/feed",
"http://feeds.japan.zdnet.com/rss/zdnet/all.rdf",
"http://20kaido.com/index.rdf",
"http://2chnode.com/rss/feed/all",
"http://akiba-souken.com/feed/all/",
"http://amaebi.net/index.rdf",
"http://amakakeru.blog59.fc2.com/?xml",
"http://artskype.com/rss/feed.xml",
"http://asitagamienai.blog118.fc2.com/?xml",
"http://beta.egmnow.com/feed/",
"http://blog.livedoor.jp/ogenre/index.rdf",
"http://blog.nicovideo.jp/atom.xml",
"http://blog.tsubuani.com/feed",
"http://blogs.adobe.com/flex/atom.xml",
"http://blogs.adobe.com/index.xml",
"http://bm.s5-style.com/feed",
"http://business.nikkeibp.co.jp/rss/all_nbo.rdf",
"http://createlier.sitemix.jp/feed/",
"http://crocro.com/news/nc.cgi?action=search&skin=rdf_srch_xml",
"http://d.hatena.ne.jp/thk/rss",
"http://damage0.blomaga.jp/index.rdf",
"http://danbooru.donmai.us/posts.atom",
"http://danbooru.donmai.us/posts.atom?tags=rss",
"http://dengekionline.com/cate/11/rss.xml",
"http://dictionary.reference.com/wordoftheday/wotd.rss",
"http://doujin-games88.net/feed",
"http://doujin.sekurosu.com/rss",
"http://dousyoko.blog.fc2.com/?xml",
"http://eroaniblog.blog.fc2.com/?xml",
"http://eroanimedougakan.blog.fc2.com/?xml",
"http://erogetrailers.com/api?md=latest",
"http://eronizimage.blog.fc2.com/?xml",
"http://erosanime.blog121.fc2.com/?xml",
"http://erotaganime.blog.fc2.com/?xml",
"http://feed.nikkeibp.co.jp/rss/nikkeibp/index.rdf",
"http://feed.rssad.jp/rss/gigazine/rss_2.0",
"http://feed.rssad.jp/rss/jcast/index.xml",
"http://feed.rssad.jp/rss/klug/fxnews/rss5.xml",
"http://feedblog.ameba.jp/rss/ameblo/yusayusa0211/rss20.xml",
"http://feeds.adobe.com/xml/rss.cfm?query=byMostRecent&languages=1",
"http://feeds.builder.japan.zdnet.com/rss/builder/all.rdf",
"http://feeds.fc2.com/fc2/xml?host=anrism.blog&format=xml",
"http://feeds.fc2.com/fc2/xml?host=kahouha2jigen.blog&format=xml",
"http://feeds.feedburner.com/gekiura",
"http://feeds.journal.mycom.co.jp/rss/mycom/index",
"http://feeds.reuters.com/reuters/JPTopNews?format=xml",
"http://galten705.blog.fc2.com/?xml",
"http://gamanjiru.net/feed",
"http://gamanjiru.net/feed/atom",
"http://gamebiz.jp/?feed=rss",
"http://gamenode.jp/rss/feed/all",
"http://ggsoku.com/feed/atom/",
"http://girlcelly.blog.fc2.com/?xml&trackback",
"http://hairana.blog.fc2.com/?xml",
"http://haruka-yumenoato.net/static/rss/index.rss",
"http://headline.harikonotora.net/rss2.xml",
"http://hentaidoujinanime.com/?xml",
"http://homepage1.nifty.com/maname/index.rdf",
"http://horiemon.com/feed/",
"http://ideahacker.net/feed/",
"http://itpro.nikkeibp.co.jp/rss/develop.rdf",
"http://itpro.nikkeibp.co.jp/rss/news.rss",
"http://itpro.nikkeibp.co.jp/rss/oss.rdf",
"http://itpro.nikkeibp.co.jp/rss/win.rdf",
"http://japan.internet.com/rss/rdf/index.rdf",
"http://jp.leopard-raws.org/rss.php",
"http://jp.techcrunch.com/feed/",
"http://kakaku.com/trendnews/rss.xml",
"http://kamisoku.blog47.fc2.com/?xml",
"http://kanesoku.com/index.rdf",
"http://kibougamotenai.blog.fc2.com/?xml",
"http://kiisu.jpn.org/rss/now.xml",
"http://konachan.com/post/piclens?page=1&tags=loli",
"http://labo.tv/2chnews/index.xml",
"http://lineblog.me/yamamotoichiro/atom.xml",
"http://majimougen.blog.fc2.com/?xml",
"http://mantan-web.jp/rss/mantan.xml",
"http://matome.naver.jp/feed/hot",
"http://matome.naver.jp/feed/tech",
"http://matome.sekurosu.com/rss",
"http://mizuhonokuni2ch.com/?xml",
"http://momoiroanime.blog.fc2.com/?xml",
"http://moroahedoujin.com/?xml",
"http://nesingazou.blog.fc2.com/?xml",
"http://newnews-moe.com/index.rdf",
"http://news.ameba.jp/index.xml",
"http://news.com.com/2547-1_3-0-5.xml",
"http://news.nicovideo.jp/?rss=2.0",
"http://news.nicovideo.jp/ranking/hot?rss=2.0",
"http://newsbiz.yahoo.co.jp/topnews.rss",
"http://nijitora.blog.fc2.com/?xml",
"http://nodvd21ver2.blog.fc2.com/?xml",
"http://orebibou.com/feed/",
"http://osu.ppy.sh/feed/ranked/",
"http://otakomu.jp/feed",
"http://pcgameconquest.blog.fc2.com/?xml",
"http://picks.dir.yahoo.co.jp/dailypicks/rss/",
"http://piknik2ch.blog76.fc2.com/?xml",
"http://plus.appgiga.jp/feed/user",
"http://purisoku.com/index.rdf",
"http://rdsig.yahoo.co.jp/RV=1/RU=aHR0cDovL3NlYXJjaHJhbmtpbmcueWFob28uY28uanAvcnNzL2J1cnN0X3JhbmtpbmctcnNzLnhtbA--;_ylt=A2RhjFhfAi9XEi0A6Glhdu57",
"http://read2ch.net/rss/",
"http://rss.dailynews.yahoo.co.jp/fc/computer/rss.xml",
"http://rss.rssad.jp/rss/akibapc/akiba-pc.rdf",
"http://rss.rssad.jp/rss/ascii/biz/rss.xml",
"http://rss.rssad.jp/rss/ascii/hobby/rss.xml",
"http://rss.rssad.jp/rss/ascii/it/rss.xml",
"http://rss.rssad.jp/rss/ascii/mac/rss.xml",
"http://rss.rssad.jp/rss/ascii/pc/rss.xml",
"http://rss.rssad.jp/rss/ascii/rss.xml",
"http://rss.rssad.jp/rss/codezine/new/20/index.xml",
"http://rss.rssad.jp/rss/forest/rss.xml",
"http://rss.rssad.jp/rss/gihyo/feed/atom",
"http://rss.rssad.jp/rss/headline/headline.rdf",
"http://rss.rssad.jp/rss/impresswatch/pcwatch.rdf",
"http://rss.rssad.jp/rss/itm/1.0/makoto.xml",
"http://rss.rssad.jp/rss/itm/1.0/netlab.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_akiba.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_android_appli.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_apple.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_facebook.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_google.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_ipad.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_iphone.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_iphone_appli.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_mixi.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_smartphone.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_twitter.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_ustream.xml",
"http://rss.rssad.jp/rss/itmbizid/1.0/bizid.xml",
"http://rss.rssad.jp/rss/itmnews/2.0/news_bursts.xml",
"http://rss.rssad.jp/rss/japaninternetcom/index.rdf",
"http://rss.rssad.jp/rss/oshietekun/atom.xml",
"http://rss.rssad.jp/rss/slashdot/slashdot.rss",
"http://rss.rssad.jp/rss/zaikeishimbun/main.xml",
"http://rssc.dokoda.jp/r/8a1dd8f128047929ba4390dab3c8065e/http/searchranking.yahoo.co.jp/realtime_buzz/",
"http://sakurabaryo.com/feed/",
"http://sankei.jp.msn.com/rss/news/points.xml",
"http://sankei.jp.msn.com/rss/news/west_points.xml",
"http://search.goo.ne.jp/rss/newkw.rdf",
"http://sekurosu.com/rss",
"http://streaming.yahoo.co.jp/rss/newly/anime/",
"http://sub0000528116.hmk-temp.com/wordpress/?feed=rss2",
"http://sukebei.nyaa.se/?page=rss&sort=2",
"http://tenshoku.mynavi.jp/knowhow/rss.xml",
"http://tensinyakimeshi.blog98.fc2.com/?xml",
"http://thefreedom12.blog41.fc2.com/?xml",
"http://togetter.com/rss/hot/culture/62",
"http://togetter.com/rss/hot/culture/63",
"http://torimatome.main.jp/blogs/comments/feed",
"http://torimatome.main.jp/blogs/feed",
"http://toshinokyouko.com/rss.php",
"http://tvanimedouga.blog93.fc2.com/?xml",
"http://uranourainformation.blog21.fc2.com/?xml",
"http://video.fc2.com/a/feed_popular.php?m=week",
"http://weather.livedoor.com/forecast/rss/area/400010.xml",
"http://wotopi.jp/feed",
"http://www.100shiki.com/feed",
"http://www.alistapart.com/rss.xml",
"http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=36",
"http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=38",
"http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=47",
"http://www.blosxom.com/?feed=rss2",
"http://www.britannica.com/eb/dailycontent/rss",
"http://www.csmonitor.com/rss/top.rss",
"http://www.ehackingnews.com/feeds/posts/default",
"http://www.falcom.co.jp/new.xml",
"http://www.famitsu.com/rss/category/fcom_game.rdf",
"http://www.famitsu.com/rss/fcom_all.rdf",
"http://www.ganganonline.com/rss/index.xml",
"http://www.ideaxidea.com/feed",
"http://www.itnews711.com/index.rdf",
"http://www.jp.playstation.com/whatsnew/whatsnew.rdf",
"http://www.keyman.or.jp/rss/v1/?rss_type=all",
"http://www.koubo.co.jp/rss.xml",
"http://www.nyaa.se/?page=rss&sort=2",
"http://www.nyaa.se/?page=rss&user=118009",
"http://www.nytimes.com/services/xml/rss/userland/HomePage.xml",
"http://www.phianime.tv/feed/",
"http://www.rebootdevelop.hr/feed/",
"http://www.rictus.com/muchado/feed/",
"http://www.sbcr.jp/atom.xml",
"http://www.slashgear.com/comments/feed/",
"http://www.torrent-anime.com/feed",
"http://www.torrent-anime.com/feed/",
"http://www.webimemo.com/feed/",
"http://www.wired.com/news_drop/netcenter/netcenter.rdf",
"http://www.xvideos.com/rss/rss.xml",
"http://www.youtube.com/rss/user/KADOKAWAanime/videos.rss",
"http://www.youtube.com/rss/user/demosouko/videos.rss",
"http://www.yukawanet.com/index.rdf",
"http://www.zou3.net/php/rss/nikkei2rss.php?head=main",
"http://xml.ehgt.org/ehtracker.xml",
"http://xml.metafilter.com/rss.xml",
"http://xvideos.2jiero.info/feed",
"http://yaraon.blog109.fc2.com/?xml",
"http://yusaani.com/home/feed/",
"http://zipdeyaruo.blog42.fc2.com/?xml",
"http://www.portalgraphics.net/rss/latest_image_list.xml",
"http://api.syosetu.com/writernovel/430380.Atom",
"http://creive.me/feed/",
"http://gihyo.jp/dev/feed/atom",
"http://gihyo.jp/feed/rss1",
"http://hakase255.blog135.fc2.com/?xml",
"http://2ch-ranking.net/rss/zenban.rdf",
"http://www.isus.jp/feed/",
"http://www.jiji.com/rss/ranking.rdf",
"http://jp.gamesindustry.biz/rss/index.xml",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCx1nAvtVDIsaGmCMSe8ofsQ",
"http://zakuzaku911.com/index.rdf",
"http://ke-tai.org/blog/feed/",
"http://data.newantenna.net/ero/rss/all.xml",
"http://developer.mixi.co.jp/feed/atom",
"http://neoneetch.blog.fc2.com/?xml",
"http://rss.itmedia.co.jp/rss/1.0/netlab.xml",
"http://netgeek.biz/feed",
"http://blog.esuteru.com/index.rdf",
"http://b.hatena.ne.jp/hotentry/game.rss",
"http://b.hatena.ne.jp/hotentry.rss",
"http://mobile.seisyun.net/rss/hot.rdf",
"http://yomi.mobi/rss/hot.rdf",
"http://saymygame.com/feed/",
"http://blog.webcreativepark.net/atom.xml",
"http://buhidoh.net/?xml",
"http://www.webcyou.com/?feed=rss2",
"http://withnews.jp/rss/consumer/new.rdf",
"https://yande.re/post/atom?tags=loli",
"http://blog.livedoor.jp/nizigami/atom.xml",
"http://nvmzaq.blog.fc2.com/?xml",
"http://keieimanga.net/index.rdf",
"http://megumi.ldblog.jp/atom.xml",
"http://kirik.tea-nifty.com/diary/index.rdf",
"http://sinri.net/comments/feed",
"http://himasoku.com/atom.xml",
"http://himasoku.com/index.rdf",
"http://20kaido.com/index.rdf",
"http://h723.blog.fc2.com/?xml",
"http://onecall2ch.com/index.rdf",
"http://www.forest.impress.co.jp/rss.xml",
"http://www.zaikei.co.jp/rss/sections/it.xml",
"http://akiba.keizai.biz/rss.xml",
"http://agag.tw/feed/2d-popular.rss",
"http://adult-vr.jp/feed/",
"http://www.anige-sokuhouvip.com/?xml",
"http://animeanime.jp/rss/index.rdf",
"http://alfalfalfa.com/index.rdf",
"http://feeds.feedburner.com/fc2/GhfA",
"http://erogetaiken072.blog.fc2.com/?xml",
"http://otanew.jp/atom.xml",
"http://jin115.com/index.rdf",
"http://www.onlinegamer.jp/rss/news.rdf",
"http://karapaia.livedoor.biz/index.rdf",
"http://getnews.jp/feed/ext/orig",
"http://www.gungho.co.jp/news/xml/rss.xml",
"http://blog.livedoor.jp/kinisoku/index.rdf",
"http://feeds.gizmodo.jp/rss/gizmodo/index.xml",
"http://himado.in/?sort=movie_id&rss=1",
"http://k-tai.impress.co.jp/cda/rss/ktai.rdf",
"http://gehasoku.com/atom.xml",
"http://feedblog.ameba.jp/rss/ameblo/principia-ca/rss20.xml",
"http://zai.diamond.jp/list/feed/rssfxnews",
"http://capacitor.blog.fc2.com/?xml",
"http://blog.livedoor.jp/vipsister23/index.rdf",
"http://vipsister23.com/atom.xml",
"http://b.hatena.ne.jp/search/tag?safe=on&q=2ch&users=500&mode=rss",
"http://b.hatena.ne.jp/search/tag?safe=on&q=%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF&users=500&mode=rss",
"http://b.hatena.ne.jp/search/tag?safe=off&q=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0&users=500&mode=rss",
"http://shikaku2ch.doorblog.jp/atom.xml",
"http://dotinstall.com/lessons.rss",
"http://2ch-ranking.net/rss/news4vip.rdf",
"http://blog.livedoor.jp/insidears/index.rdf",
"http://2ch-ranking.net/rss/newsplus.rdf",
"http://2ch-ranking.net/rss/news.rdf",
"http://nullpoantenna.com/game.rdf",
"http://workingnews.blog117.fc2.com/?xml",
"http://bm.s5-style.com/feed",
"http://2ch-ranking.net/rss/ghard.rdf",
"http://www.724685.com/blog/rss.xml",
"http://www.yukawanet.com/index.rdf",
"http://2ch-ranking.net/rss/bizplus.rdf",
"http://www.nicovideo.jp/ranking/fav/daily/all?rss=2.0&lang=ja-jp",
"http://www.tarikin.net/rss0.rdf",
"http://blog.livedoor.jp/dqnplus/index.rdf",
"http://www.seojapan.com/blog/feed",
"http://2ch-ranking.net/rss/morningcoffee.rdf",
"http://2ch-ranking.net/mt50k.rdf",
"http://rssblog.ameba.jp/yandereotto/rss20.xml",
"https://business.nikkei.com/rss/sns/nb.rdf",
"http://daredemopc.blog51.fc2.com/?xml",
"http://erogetaikenban.blog65.fc2.com/?xml",
"http://news.goo.ne.jp/rss/topstories/gootop/index.rdf",
"http://lanovelien.blog121.fc2.com/?xml",
"http://news.livedoor.com/topics/rss/eco.xml",
"http://ragnarokonline.gungho.jp/index.rdf",
"http://rocketnews24.com/feed/",
"https://news.denfaminicogamer.jp/feed",
"http://www.igda.jp/?feed=rss2",
"http://feeds.cnn.co.jp/cnn/rss"
];
// ==== 既存データのロード ====
if (!localStorage.getItem('verse_shared_rssFeeds')) {
localStorage.setItem('verse_shared_rssFeeds', JSON.stringify(PRESET_RSS));
}
// アプリ状態
let users = JSON.parse(localStorage.getItem('verse_users') || '[]');
let currentUser = JSON.parse(localStorage.getItem('verse_currentUser') || 'null');
let posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
let isDarkMode = localStorage.getItem('verse_darkMode') === 'true';
let isInitialized = false;
// RSS/BOT状態
let botInterval = null;
let rssInterval = null;
// 共有RSS設定
let sharedRssFeeds = JSON.parse(localStorage.getItem('verse_shared_rssFeeds') || '[]');
let sharedRssInterval = Number(localStorage.getItem('verse_shared_rssInterval')) || 300;
let sharedRssLastIds = JSON.parse(localStorage.getItem('verse_shared_rssLastIds') || '{}');
// 各フィードのON/OFF(true=ON)。未設定はデフォルトON。
let sharedRssEnabled = JSON.parse(localStorage.getItem('verse_shared_rssEnabled') || '{}');
// ===== 認証UI =====
function showAuthModal(mode = 'login', errorMsg = '') {
document.getElementById('auth-title').textContent = (mode === 'register') ? '新規登録' : 'ログイン';
document.getElementById('auth-form').authMode = mode;
document.getElementById('auth-email').value = '';
document.getElementById('auth-password').value = '';
document.getElementById('auth-modal').style.display = '';
document.getElementById('main-content').style.display = 'none';
document.getElementById('auth-error').textContent = errorMsg || '';
document.getElementById('auth-error').style.display = errorMsg ? '' : 'none';
document.getElementById('toggle-auth-mode').textContent = (mode === 'register') ? 'ログインはこちら' : '新規登録はこちら';
}
function hideAuthModal() {
document.getElementById('auth-modal').style.display = 'none';
document.getElementById('main-content').style.display = '';
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('auth-form').onsubmit = function(e) {
e.preventDefault();
const email = document.getElementById('auth-email').value.trim().toLowerCase();
const password = document.getElementById('auth-password').value;
if (!email || !password) {
showAuthModal(this.authMode, 'メールアドレスとパスワードを入力してください');
return;
}
if (this.authMode === 'register') {
if (users.find(u => u.email === email)) {
showAuthModal('register', 'このメールアドレスは既に登録されています');
return;
}
const newUser = {
email,
password,
profile: { icon: 'https://via.placeholder.com/100', username: email.split('@')[0], selfIntro: '' }
};
users.push(newUser);
localStorage.setItem('verse_users', JSON.stringify(users));
currentUser = { email };
localStorage.setItem('verse_currentUser', JSON.stringify(currentUser));
showAuthModal('login', '登録完了!ログインしてください');
} else {
const user = users.find(u => u.email === email && u.password === password);
if (!user) { showAuthModal('login', 'メールアドレスまたはパスワードが違います'); return; }
currentUser = { email };
localStorage.setItem('verse_currentUser', JSON.stringify(currentUser));
hideAuthModal();
initializeApp();
}
};
document.getElementById('toggle-auth-mode').onclick = function() {
const mode = (document.getElementById('auth-title').textContent === '新規登録') ? 'login' : 'register';
showAuthModal(mode);
};
if (!currentUser) showAuthModal('login'); else { hideAuthModal(); initializeApp(); }
});
function logout() {
localStorage.removeItem('verse_currentUser');
currentUser = null;
stopRssAuto();
showAuthModal('login');
}
// ===== 初期化 =====
function initializeApp() {
if (isInitialized) return;
if (!currentUser) { showAuthModal('login'); return; }
users = JSON.parse(localStorage.getItem('verse_users') || '[]');
posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
isDarkMode = localStorage.getItem('verse_darkMode') === 'true';
const user = users.find(u => u.email === currentUser.email);
window.profile = user ? user.profile : { icon: 'https://via.placeholder.com/100', username: 'ゲストユーザー', selfIntro: '' };
updateAllUI();
updateStatusIndicators();
updateRssUI();
if (isDarkMode) {
document.body.classList.add('dark');
document.body.style.background = 'linear-gradient(135deg, #1a202c 0%, #2d3748 100%)';
} else {
document.body.classList.remove('dark');
document.body.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
}
document.getElementById('main-content').style.display = '';
document.getElementById('logout-btn').style.display = '';
isInitialized = true;
startRssAuto();
addLog('bot-log', 'BOT機能初期化完了', 'success');
addLog('rss-log', 'RSS自動投稿(全体共有)初期化完了', 'success');
}
// ===== 投稿(ユーザー/BOT) =====
function createUserPost() {
const ta = document.getElementById('postContent');
const txt = ta.value.trim();
if (!txt) return alert('投稿内容を入力してください。');
if (!currentUser) return alert('ログインが必要です。');
createPost(txt, 'user', profile.username, profile.icon);
ta.value = '';
updateCharCount();
}
function createPost(content, type = 'user', username = null, icon = null, extra = {}) {
if (!content || !content.trim()) return false;
const post = {
id: Date.now() + Math.random(),
content: content.trim(),
likes: 0,
timestamp: new Date().toLocaleString('ja-JP'),
type,
username: username || profile.username,
icon: icon || profile.icon,
userEmail: currentUser ? currentUser.email : '',
...extra
};
posts.unshift(post);
saveData();
renderTimeline();
return true;
}
function likePost(id) {
const idx = posts.findIndex(p => p.id === id);
if (idx >= 0) {
posts[idx].likes++;
saveData();
renderTimeline();
}
}
function deletePost(id) {
if (!confirm('この投稿を削除しますか?')) return;
posts = posts.filter(p => p.id !== id);
saveData();
renderTimeline();
}
function clearAllPosts() {
if (!confirm('全ての投稿を削除しますか?')) return;
posts = [];
saveData();
renderTimeline();
}
function exportData() {
const blob = new Blob([JSON.stringify(posts, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'verse_posts.json';
a.click();
URL.revokeObjectURL(url);
}
// ===== タイムライン描画(JSX混入の修正・無害化) =====
function escapeHtml(s) {
return (s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[m]));
}
function renderTimeline() {
const tl = document.getElementById('timeline');
const emp = document.getElementById('empty-timeline');
const cnt = document.getElementById('post-count');
if (!tl || !emp || !cnt) return;
const allPosts = posts;
if (allPosts.length === 0) {
tl.innerHTML = '';
emp.style.display = 'block';
cnt.textContent = '(0件の投稿)';
return;
}
emp.style.display = 'none';
cnt.textContent = `(${allPosts.length}件の投稿)`;
tl.innerHTML = allPosts.map(p => {
const info = { bot: '🤖 BOT', markov: '🎲 MarkovBOT', user: '👤 ユーザー', feed: '📰 FEEDBOT' }[p.type] || '👤';
const main = p.link
? `<a href="${p.link}" target="_blank" class="text-blue-600 underline">${escapeHtml(p.content)}</a>`
: `${escapeHtml(p.content)}`;
return `
<div class="timeline-post p-6">
<div class="flex justify-between items-start mb-4">
<div class="flex items-center space-x-3">
<img src="${p.icon}" class="w-10 h-10 rounded-full object-cover" onerror="this.src='https://via.placeholder.com/40'">
<div>
<div class="flex items-center">
<span class="font-semibold text-gray-800 dark:text-white">${escapeHtml(p.username)}</span>
<span class="username-badge">${info}</span>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${p.timestamp}</div>
</div>
</div>
</div>
<div class="text-gray-800 dark:text-gray-200 mb-4 leading-relaxed">${main}</div>
<div class="flex items-center space-x-4 pt-4 border-t border-gray-100 dark:border-gray-600">
<button onclick="likePost(${p.id})" class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-red-500">
<i class="fas fa-heart"></i><span>${p.likes}</span>
</button>
<div class="relative">
<button onclick="toggleShareMenu(${p.id})" class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-blue-500">
<i class="fas fa-share"></i><span>シェア</span>
</button>
<div id="share-menu-${p.id}" class="share-menu hidden">
<button onclick="shareToX(${p.id})"><i class="fab fa-x-twitter text-blue-400 mr-2"></i>Xでシェア</button>
<button onclick="shareToLine(${p.id})"><i class="fab fa-line text-green-400 mr-2"></i>LINEでシェア</button>
<button onclick="copyPost(${p.id})"><i class="fas fa-copy mr-2"></i>コピー</button>
</div>
</div>
<button onclick="deletePost(${p.id})" class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-red-500 ml-auto">
<i class="fas fa-trash"></i><span>削除</span>
</button>
</div>
</div>
`;
}).join('');
}
function getPostContentText(id) {
const p = posts.find(x => x.id === id);
if (!p) return '';
const tmp = document.createElement('div');
tmp.innerHTML = p.content;
return tmp.textContent || tmp.innerText || '';
}
function toggleShareMenu(id) {
document.querySelectorAll('[id^="share-menu-"]')
.forEach(el => el.classList.add('hidden'));
const m = document.getElementById('share-menu-' + id);
if (m) m.classList.toggle('hidden');
}
function shareToX(id) {
const t = encodeURIComponent(getPostContentText(id));
const u = encodeURIComponent(location.href);
window.open(`https://twitter.com/intent/tweet?text=${t}&url=${u}`, '_blank');
}
function shareToLine(id) {
const u = encodeURIComponent(location.href);
window.open(`https://social-plugins.line.me/lineit/share?url=${u}`, '_blank');
}
function copyPost(id) {
const t = getPostContentText(id);
if (navigator.clipboard) {
navigator.clipboard.writeText(t).then(() => alert('コピーしました')).catch(() => fallbackCopy(t));
} else fallbackCopy(t);
}
function fallbackCopy(t) {
const ta = document.createElement('textarea');
ta.value = t; document.body.appendChild(ta);
ta.select(); document.execCommand('copy');
document.body.removeChild(ta);
alert('コピーしました');
}
// ===== RSS UI(個別ON/OFF + 一括ON/OFF) =====
function updateRssUI() {
const listDiv = document.getElementById('rss-list');
if (!listDiv) return;
if (!sharedRssFeeds || sharedRssFeeds.length === 0) {
listDiv.innerHTML = '<div class="text-white text-xs opacity-80">RSSフィード未登録</div>';
} else {
listDiv.innerHTML = sharedRssFeeds.map((url, i) => {
const enabled = sharedRssEnabled[url] !== false; // 既定ON
const enc = encodeURIComponent(url);
return `
<div class="flex items-center space-x-2 bg-white bg-opacity-90 rounded px-2 py-2 mb-1">
<input type="checkbox" ${enabled ? 'checked' : ''} onchange="toggleRssEnabled('${enc}', this.checked)" title="ON/OFF">
<div class="truncate flex-1 text-xs" title="${escapeHtml(url)}">${escapeHtml(url)}</div>
<button onclick="delRssFeed(${i})" class="text-red-500 hover:text-red-700" title="削除"><i class="fas fa-trash"></i></button>
</div>
`;
}).join('');
}
const iv = document.getElementById('rss-interval');
if (iv) iv.value = sharedRssInterval || 300;
}
function toggleRssEnabled(encUrl, on) {
const url = decodeURIComponent(encUrl);
sharedRssEnabled[url] = !!on;
saveSharedRss();
addLog('rss-log', `FEED ${on ? 'ON' : 'OFF'}: ${url}`, 'info');
}
function setAllRssEnabled(on) {
(sharedRssFeeds || []).forEach(u => sharedRssEnabled[u] = !!on);
saveSharedRss();
updateRssUI();
addLog('rss-log', `全フィードを${on ? 'ON' : 'OFF'}にしました`, 'success');
}
function saveSharedRss() {
localStorage.setItem('verse_shared_rssFeeds', JSON.stringify(sharedRssFeeds));
localStorage.setItem('verse_shared_rssInterval', String(sharedRssInterval));
localStorage.setItem('verse_shared_rssLastIds', JSON.stringify(sharedRssLastIds));
localStorage.setItem('verse_shared_rssEnabled', JSON.stringify(sharedRssEnabled));
}
function addRssFeed() {
const url = document.getElementById('rss-url').value.trim();
if (!/^https?:\/\/.+/.test(url)) { addLog('rss-log', '正しいRSSフィードURLを入力してください', 'error'); return; }
if (!sharedRssFeeds) sharedRssFeeds = [];
if (sharedRssFeeds.includes(url)) { addLog('rss-log', 'すでに登録済みです', 'error'); return; }
sharedRssFeeds.push(url);
sharedRssEnabled[url] = true; // 既定ON
saveSharedRss();
updateRssUI();
addLog('rss-log', `RSS追加: ${url}`, 'success');
document.getElementById('rss-url').value = '';
}
function delRssFeed(i) {
if (!sharedRssFeeds[i]) return;
if (!confirm('このフィードを削除しますか?')) return;
const url = sharedRssFeeds[i];
sharedRssFeeds.splice(i, 1);
delete sharedRssEnabled[url];
delete sharedRssLastIds[url];
saveSharedRss();
updateRssUI();
addLog('rss-log', 'RSS削除', 'info');
}
function setRssInterval() {
const iv = document.getElementById('rss-interval').valueAsNumber || 300;
if (iv < 10) return alert('間隔は10秒以上で設定してください。');
sharedRssInterval = iv;
saveSharedRss();
updateRssUI();
startRssAuto();
addLog('rss-log', `自動投稿間隔を${iv}秒に設定`, 'success');
}
function fetchRssNow() { fetchRssFeeds(); }
// ===== RSS取得(OFFのフィードはスキップ) =====
function fetchRssFeeds() {
if (!sharedRssFeeds || sharedRssFeeds.length === 0) return;
sharedRssFeeds.forEach(feedUrl => {
if (sharedRssEnabled[feedUrl] === false) {
addLog('rss-log', `OFFのため取得スキップ: ${feedUrl}`, 'info');
return;
}
fetch('https://api.rss2json.com/v1/api.json?rss_url=' + encodeURIComponent(feedUrl))
.then(resp => resp.json())
.then(data => {
if (!data.items || !data.items.length) return;
let lastId = sharedRssLastIds[feedUrl] || '';
let newItems = [];
for (const item of data.items) {
const guid = item.guid || item.link || item.pubDate || item.title;
if (!lastId || String(guid) > String(lastId)) newItems.push(item);
}
if (newItems.length === 0) return;
newItems.reverse().forEach(item => {
const guid = item.guid || item.link || item.pubDate || item.title;
// FEED本文は省略(タイトル+リンクのみポスト)
if (!posts.some(p => p.type === 'feed' && p.link === item.link)) {
createPost(item.title, 'feed', 'FEEDBOT', 'https://cdn-icons-png.flaticon.com/512/3416/3416046.png', { link: item.link });
addLog('rss-log', `新しい記事: ${item.title}`, 'success');
}
sharedRssLastIds[feedUrl] = guid;
});
saveSharedRss();
})
.catch(() => addLog('rss-log', 'RSS取得エラー: ' + feedUrl, 'error'));
});
}
function startRssAuto() {
stopRssAuto();
fetchRssFeeds();
rssInterval = setInterval(fetchRssFeeds, (sharedRssInterval || 300) * 1000);
updateStatusIndicators();
addLog('rss-log', `RSS自動投稿を開始 (${sharedRssInterval}秒間隔)`, 'success');
}
function stopRssAuto() {
if (rssInterval) clearInterval(rssInterval);
rssInterval = null;
updateStatusIndicators();
addLog('rss-log', 'RSS自動投稿を停止しました', 'info');
}
// ===== BOT =====
function postBotMessage() {
const ta = document.getElementById('botContent');
const txt = ta.value.trim();
if (!txt) return alert('BOT投稿内容を入力してください。');
if (!currentUser) return alert('ログインが必要です。');
if (createPost(txt, 'bot', 'BOT', profile.icon)) {
ta.value = '';
addLog('bot-log', `BOT投稿: "${txt.substring(0, 30)}..."`, 'success');
}
}
function generateMarkovText() {
// FEED(外部記事)は学習対象から除外し、ユーザーとBOT投稿のみ学習
let text = posts
.filter(p => ['user','bot'].includes(p.type))
.map(p => {
const d = document.createElement('div');
d.innerHTML = p.content;
return (d.textContent || d.innerText || '')
.replace(/\s+/g, ' ').replace(/https?:\/\/\S+/g, '').trim();
}).join(' ');
if (text.length < 20) {
const fallbacks = ["今日はいい天気ですね!","最近面白いニュースありましたか?","新しいアイデアが浮かんできました。","皆さんはどう思いますか?"];
return fallbacks[Math.floor(Math.random() * fallbacks.length)];
}
const tokens = text.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\w]+|[。、!?\r\n]/g) || [];
if (tokens.length < 2) return tokens.join('');
const markov = {};
for (let i = 0; i < tokens.length - 2; i++) {
const key = tokens[i] + '|' + tokens[i+1];
if (!markov[key]) markov[key] = [];
markov[key].push(tokens[i+2]);
}
let idx = Math.floor(Math.random() * (tokens.length - 2));
let key = tokens[idx] + '|' + tokens[idx+1];
let result = [tokens[idx], tokens[idx+1]];
let maxLen = 60 + Math.floor(Math.random() * 40);
for (let i = 0; i < maxLen; i++) {
const nexts = markov[key];
if (!nexts || nexts.length === 0) break;
const next = nexts[Math.floor(Math.random() * nexts.length)];
result.push(next);
if (/[。!?\n]/.test(next)) break;
key = result[result.length - 2] + '|' + result[result.length - 1];
}
return result.join('').replace(/\n/g, '');
}
function postMarkovBot() {
const txt = generateMarkovText();
if (createPost(txt, 'markov', 'MarkovBOT', profile.icon)) {
addLog('bot-log', `マルコフ投稿: "${txt.substring(0, 40)}..."`, 'success');
}
}
function startBotAutoPost() {
const iv = document.getElementById('botIntervalSec').valueAsNumber || 60;
if (iv < 10) { alert('間隔は10秒以上で設定してください。'); return; }
stopBotAutoPost();
setTimeout(postMarkovBot, 3000);
botInterval = setInterval(postMarkovBot, iv * 1000);
updateStatusIndicators();
addLog('bot-log', `マルコフBOT自動投稿開始 (${iv}秒間隔)`, 'success');
}
function stopBotAutoPost() {
if (botInterval) {
clearInterval(botInterval);
botInterval = null;
updateStatusIndicators();
addLog('bot-log', 'マルコフBOT自動投稿を停止しました', 'info');
}
}
// ===== 共通UI/保存 =====
function addLog(id, msg, type = 'info') {
const el = document.getElementById(id);
const ts = new Date().toLocaleTimeString('ja-JP');
const div = document.createElement('div');
const cls = { error: 'error-message', success: 'success-message', info: 'text-white opacity-90' }[type] || 'text-white opacity-90';
div.className = cls;
div.innerHTML = `<span class="opacity-75">[${ts}]</span> ${escapeHtml(msg)}`;
if (el) {
el.appendChild(div);
el.scrollTop = el.scrollHeight;
while (el.children.length > 100) el.removeChild(el.firstChild);
}
try { console.log(`[${ts}] ${msg}`); } catch(_) {}
}
function updateStatusIndicators() {
const botI = document.getElementById('bot-status');
const botT = document.getElementById('bot-status-text');
if (botI && botT) {
const active = botInterval !== null;
botI.className = `status-indicator ${active ? 'status-active' : 'status-inactive'}`;
botT.textContent = active ? '動作中' : '停止中';
}
const rssI = document.getElementById('rss-status');
const rssT = document.getElementById('rss-status-text');
if (rssI && rssT) {
const active = rssInterval !== null;
rssI.className = `status-indicator ${active ? 'status-active' : 'status-inactive'}`;
rssT.textContent = active ? '動作中' : '停止中';
}
}
function showSystemStatus() {
alert(
`=== Verse システムステータス ===\n全体投稿数: ${posts.length}\nRSS登録数: ${sharedRssFeeds.length}\nBOT投稿数: ${posts.filter(p => ['bot', 'markov'].includes(p.type)).length}\n\nBOT自動投稿: ${botInterval ? '動作中' : '停止中'}\nRSS自動投稿: ${rssInterval ? '動作中' : '停止中'}`
);
}
function uploadProfileIcon(e) {
const f = e.target.files[0];
if (!f) return;
if (f.size > 5 * 1024 * 1024) { alert('5MB以下にしてください。'); return; }
const r = new FileReader();
r.onload = () => {
profile.icon = r.result;
saveProfileNoAlert();
updateAllUI();
alert('プロフィール画像更新!');
};
r.readAsDataURL(f);
}
function saveProfile() {
const un = document.getElementById('username').value.trim();
const si = document.getElementById('self-intro').value.trim();
if (un.length > 20) { alert('ユーザー名は20文字以内で。'); return; }
profile.username = un || 'ゲストユーザー';
profile.selfIntro = si;
saveProfileNoAlert();
updateAllUI();
alert('プロフィール保存!');
}
function saveProfileNoAlert() {
users = JSON.parse(localStorage.getItem('verse_users') || '[]');
const idx = users.findIndex(u => u.email === (currentUser && currentUser.email));
if (idx >= 0) {
users[idx].profile = profile;
localStorage.setItem('verse_users', JSON.stringify(users));
}
}
function updateAllUI() {
const pi = document.getElementById('profile-icon');
const hi = document.getElementById('header-profile-icon');
if (pi) pi.src = profile.icon;
if (hi) hi.src = profile.icon;
['username-preview','header-username'].forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = profile.username;
});
const sip = document.getElementById('self-intro-preview');
if (sip) sip.textContent = profile.selfIntro || 'まだ自己紹介がありません';
const emailEl = document.getElementById('header-user-email');
if (emailEl && currentUser) emailEl.textContent = currentUser.email;
renderTimeline();
}
function updateCharCount() {
const pc = document.getElementById('postContent');
const cc = document.getElementById('char-count');
if (pc && cc) {
const l = pc.value.length;
cc.textContent = `(${l}/500)`;
cc.style.color = l > 450 ? '#ef4444' : '';
}
}
function toggleDarkMode() {
isDarkMode = !isDarkMode;
if (isDarkMode) {
document.body.classList.add('dark');
document.body.style.background = 'linear-gradient(135deg, #1a202c 0%, #2d3748 100%)';
} else {
document.body.classList.remove('dark');
document.body.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
}
localStorage.setItem('verse_darkMode', isDarkMode.toString());
}
function saveData() { localStorage.setItem('verse_posts', JSON.stringify(posts)); }
// 入力UIフック
document.addEventListener('DOMContentLoaded', () => {
const pc = document.getElementById('postContent');
if (pc) {
pc.addEventListener('input', updateCharCount);
pc.addEventListener('keydown', e => { if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); createUserPost(); } });
}
const ui = document.getElementById('username');
if (ui) ui.addEventListener('input', () => {
const v = ui.value.trim() || 'ゲストユーザー';
['username-preview','header-username'].forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = v;
});
});
const si = document.getElementById('self-intro');
if (si) si.addEventListener('input', () => {
const v = si.value.trim() || 'まだ自己紹介がありません';
const el = document.getElementById('self-intro-preview');
if (el) el.textContent = v;
});
});
// 終了処理・エラー
window.addEventListener('beforeunload', () => { stopBotAutoPost(); stopRssAuto(); saveData(); });
window.addEventListener('error', e => { addLog('bot-log', `システムエラー: ${e.message}`, 'error'); });
</script>
</body>
</html>
ロボットの作り方
1) まずはタイプを決める(おすすめ)
- ✅ 2輪差動ローバー … 作りやすい・学びやすい(おすすめ)
- 4輪/キャタピラ … 段差に強い
- ロボットアーム … 物体操作に特化
- 二足/人型 … 超上級(制御が難しい)
以下は「2輪ローバー」の具体例です。
2) 必要な部品(例)
- マイコン:Arduino Uno か Nano
- モータードライバ:TB6612FNG(効率◎/L298Nより低発熱)
- DCギヤドモータ×2(TTモータ 6V~9V)+ホイール×2、キャスタ車輪×1
- 距離センサ:HC-SR04(超音波)
- 電源:18650×2(7.4V)+スイッチ *Arduino用に5V 降圧モジュールあると安定
(または6V~9V電池+ArduinoのVIN) - ジャンパ線、ブレッドボード、シャーシ
3) 配線(TB6612FNG+Arduino)
電源系
- VM → モータ用バッテリ+(6–12V)
- VCC → Arduino 5V
- GND → Arduino GND(必ず共通GNDに)
制御ピン(例)
- STBY → 5V(有効化)
- PWMA → D5(PWM)/AIN1 → D7/AIN2 → D8(左モータ)
- PWMB → D6(PWM)/BIN1 → D9/BIN2 → D10(右モータ)
超音波 HC-SR04
- VCC → 5V、GND → GND
- Trig → D3、Echo → D2
4) サンプルコード(障害物回避・そのまま書き込んでOK)
// 2輪ローバー 障害物回避(Arduino UNO)
// Driver: TB6612FNG / Sensor: HC-SR04
// 左モータ(A側)
const int AIN1 = 7;
const int AIN2 = 8;
const int PWMA = 5; // PWM
// 右モータ(B側)
const int BIN1 = 9;
const int BIN2 = 10;
const int PWMB = 6; // PWM
// 超音波
const int TRIG = 3;
const int ECHO = 2;
// 速度(0-255)
const int SPD_FWD = 160;
const int SPD_TURN = 150;
const int SPD_BACK = 150;
// 閾値
const int OBST_CM = 20;
long readDistanceCm() {
// 超音波測距
digitalWrite(TRIG, LOW); delayMicroseconds(2);
digitalWrite(TRIG, HIGH); delayMicroseconds(10);
digitalWrite(TRIG, LOW);
long dur = pulseIn(ECHO, HIGH, 30000UL); // 30msタイムアウト
if (dur == 0) return 400; // 計測失敗=遠い扱い
long cm = dur * 0.034 / 2;
return cm;
}
void setMotorRaw(int in1, int in2, int pwmPin, int spd) {
if (spd < 0) { // 後退
digitalWrite(in1, LOW);
digitalWrite(in2, HIGH);
analogWrite(pwmPin, constrain(-spd, 0, 255));
} else { // 前進/停止
digitalWrite(in1, HIGH);
digitalWrite(in2, LOW);
analogWrite(pwmPin, constrain(spd, 0, 255));
}
}
// 左右速度(-255~255)
void drive(int left, int right) {
setMotorRaw(AIN1, AIN2, PWMA, left);
setMotorRaw(BIN1, BIN2, PWMB, right);
}
void stopAll() { drive(0, 0); }
void setup() {
pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT); pinMode(PWMA, OUTPUT);
pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT); pinMode(PWMB, OUTPUT);
pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT);
// 乱数初期化(浮遊ピンから)
randomSeed(analogRead(A0));
stopAll();
}
void loop() {
long d = readDistanceCm();
if (d < OBST_CM) {
// 障害物あり:停止→後退→ランダム旋回
stopAll(); delay(100);
drive(-SPD_BACK, -SPD_BACK); delay(300);
stopAll(); delay(50);
bool turnLeft = random(0, 2) == 0;
if (turnLeft) {
drive(-SPD_TURN, SPD_TURN); // 左旋回
} else {
drive(SPD_TURN, -SPD_TURN); // 右旋回
}
delay(300);
stopAll(); delay(50);
} else {
// 前進
drive(SPD_FWD, SPD_FWD);
}
delay(20);
}
5) 動作チェック手順(短縮版)
- 片輪ずつ回るか確認(片方のPWMだけ
analogWriteして正逆転をチェック) - 両輪前進→後退→旋回が正しいか
- 超音波の距離が変化するか(手を近づけて
OBST_CM未満で回避するか)
6) よくあるハマりどころ
- GND共通忘れ:Arduinoとモータ電源のGNDは必ず共通
- 電圧不足:モータとロジックは分離電源が安定(降圧5VでArduino)
- 配線逆:AIN1/AIN2, BIN1/BIN2が逆だと回転方向が反転(コード側で符号を入れ替えてもOK)
- 超音波の誤計測:屋外/柔らかい物体は反射弱い→閾値を上げる/IRセンサ併用
7) 発展(やりたい順に追加)
- ライン追従:IRセンサ(TCRT5000)を2~5個
- エンコーダ:速度/距離のフィードバック制御(PID)
- IMU:姿勢安定(MPU-6050/9250)
- Raspberry Pi + ROS2:カメラ搭載、自己位置推定/SLAM、スマホから操作
- ロボットアーム:サーボ(MG996R等)+グリッパでピック&プレース
- 通信:ESP32でWi-Fi/BLEリモコン、Web UI 操作
必要なら、配線図の簡易図やライン追従/Bluetooth操作の追加コードもこのまま出せます。どの方向(カメラ/アーム/ROS2 など)に伸ばしたいかだけ教えてくれたら、そのルートに最適化した設計とコードを用意するよ。
AIillustration
holding_sword_girlwomanfemale_8_years_old_brown_hair_red_pupils,
high quality, very high resolution, large filesize,
high detailed,
(distant wide shot)++,
straight hair, dark hair, very long hair, smile, flat chest, expressionless,
white dress, frill, very long dress,
music live,
(Uyuni salt lake)++, walking on the water,
(Rembrandt lighting)++, colorful refraction, (lens flare)++, bloom, (film Reflection)+++,
ネガティブプロンプト
lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts,signature, watermark, username, blurry, artist name, multiple legs, malformation, close up,
big breasts, loli, glasses, sun,
立ち絵
【プロンプト】
masterpiece, super fine illustration, extremely detailed, 1girl, flat background, full body, standing, extremely detailed face and eyes,
【ネガティブプロンプト】
flat color, flat shading, nsfw, retro style, poor quality, worst quality, bad face, bad fingers, bad anatomy, missing fingers, low res, cropped, signature, watermark, username, artist name, text
ポジティブ
girlwomanfemale_littlechild10_years_old_green_hair_pointy_ears_blue_eyes_Gemological_ornaments_,high quality, very high resolution, large filesize,
high detailed,
(distant wide shot)++,
straight hair, dark hair, very long hair, smile, flat chest, expressionless,
white dress, frill, very long dress,
music live,
(Uyuni salt lake)++, walking on the water,
(Rembrandt lighting)++, colorful refraction, (lens flare)++, bloom, (film Reflection)+++,
ネガティブ
lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts,signature, watermark, username, blurry, artist name, multiple legs, malformation, close up,
big breasts, loli, glasses, sun,
girlwomanfemale_littlechild10_years_old_pointy_ears_blue_eyes_Gemological_ornaments_high_quality_very_hig_ornaments_,high quality, very high resolution, large filesize,
high detailed,
(distant wide shot)++,
straight hair, dark hair, very long hair, smile, flat chest, expressionless,
white dress, frill, very long dress,
music live,
(Uyuni salt lake)++, walking on the water,
(Rembrandt lighting)++, colorful refraction, (lens flare)++, bloom, (film Reflection)+++,
ネガティブ
lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts,signat/*ure, watermark, username, blurry, artist name, multiple legs, malformation, close up,
big breasts, loli, glasses, sun,
HIGHRESOTION
GPT-4
$20.00
masterpiece,high quality,1 girl,full body,standing,blonde long hair,extremely detailed face,smile,simple background,wallpaper
flat color, flat shading, nsfw,(worst quality, low quality)1.4, interlocked fingers, (zombie, sketch, comic)+
【プロンプト】
masterpiece, super fine illustration, extremely detailed, 1girl, flat background, full body, standing, extremely detailed face and eyes,
【ネガティブプロンプト】
flat color, flat shading, nsfw, retro style, poor quality, worst quality, bad face, bad fingers, bad anatomy, missing fingers, low res, cropped, signature, watermark, username, artist name, text
(best quality)+,(masterpiece)++,(ultra detailed)++, ((Add layer)), sunny lighting, BREAK,
Facial expression: A girl with a happy expression stands in the forest surrounded by colorful flowers.
Hairstyle: The girl’s long hair is styled in two braids tied with red ribbons.
Clothing: She wears a light-colored summer dress adorned with colorful flower prints. She also wears a straw hat.
Weather conditions: A clear blue sky with some scattered clouds overhead, suggesting bright sunshine and a refreshing breeze.
Building conditions: None seen, there’s a small rocky hill in the background.
Terrain conditions: Rolling green hills covered by wildflowers surround the small valley where the girl stands.
Lighting: The scene is illuminated with bright, warm light, suggesting mid-morning or early afternoon on a sunny day.
Art style: Animation
nice hands, perfect hands,
Create a digitally painted Bishojo game CG featuring a stunning gal with big eyes, reminiscent of a beautiful character from a gal game.
A beautiful forest where mischievous spirits reside. A wizard girl stands amidst the enchanting foliage with an intrigued yet cautious expression. She has long flowing hair, cascading like waves. She wears a majestic, emerald green robe adorned with intricate golden patterns. The weather is calm, with dappled sunlight filtering through the canopy. The buildings in the forest are whimsical treehouses, blending seamlessly with nature. The terrain consists of winding pathways and moss-covered rocks. Soft, ethereal lighting surrounds the girl, accentuating her magical aura in this enchanting animation style.
キリスト教
創世記
出エジプト記
レビ記
民数記
申命記
ヨシュア記
士師記
ルツ記
サムエル記上・下
列王記上・下
歴代誌上・下
エズラ記
ネヘミヤ記
エステル記
➡ 15. ヨブ記
詩篇
箴言
伝道の書(コヘレトの言葉)
雅歌(ソロモンの歌)
1000兆円稼ぐ方法
全体設計(コンセプト)
- 形態:持株会社+ベンチャースタジオ+インフラ投資(Berkshire×TSMC×OpenAI×国家インフラ)
- 狙い:
- 需要が硬い「土台(エネルギー・半導体・物流)」
- 高粗利でスケールが速い「AI・ソフト」
- ネットワーク効果で独占力を持つ「決済・プラットフォーム」
- 評価の源泉:キャッシュフロー×耐久性×ネットワーク効果(=高マルチプル)
8本柱(“お金の方程式”つき)
数字はあくまでオーダー感。複数柱の合算で年商・年EBITDAを数十兆円級に乗せ、時価総額→1000兆円級を狙います。
- AIエージェント(業務自動化SaaS)
- 式:利用者5,000万社×月3,000円×12=1.8兆円/年
- 逆張り:中小企業の“面倒”業務(請求・在庫・CS)を丸ごと自動運転。解約率↓、粗利80%超狙い。
- ロボティクス×サブスク(“ロボット労働”の販売)
- 式:1,000万台×年100万円=10兆円/年
- 物流・製造・介護・警備をターゲット。機体は原価回収後はほぼサブスク利益。
- エネルギーギガプロジェクト(再エネ+蓄電+系統)
- 仮:200GW×稼働30%=約525TWh/年
- 利ざや10円/kWhなら約5.2兆円/年売上
- 送配電・蓄電で安定キャッシュ化。グリーンボンドでレバレッジ。
- 半導体(先端パッケージ+専用AIチップ)
- “全部TSMC”は非現実。先端パッケージ/CoWoS/HBM、国産特化SoCに絞る。
- 数年で売上数兆円級、戦略的価値で高マルチプル。
- 決済・送金ネットワーク(超低手数料で面積を取る)
- 式:年間総決済額1,000兆円×テイク0.3%=3兆円/年
- B2B請求・越境送金・マイクロペイ。規制対応を強みに。
- ヘルスケア×ドラッグディスカバリープラットフォーム
- AI創薬+受託製造(CMO/CDMO)。1件当たりのライセンス+ロイヤリティ蓄積。
- 当たり数本でロイヤリティ累積数兆円の“地層”を作る。
- 自動化物流(MFC網+ラストワンマイル)
- 式:1,000拠点×拠点EBITDA50億円=5兆円/年
- ロボ倉庫+ダークストア+ドローン/UGV。食品・医薬で粘着性UP。
- IP・メディア・ゲーム(AI生成×強運営)
- 長期運用IPを“工業化”。ゲーム/アニメ/VTuber/UGCの多層マネタイズ。
- 単体で数千億〜1兆円/年級を複数本(あなたの得意分野を核にできる)
これらを同時に薄くやらない。2〜3本で突破口→残りを買収/合弁で増やすのが王道。
フェーズ別ロードマップ(目安)
0〜12ヶ月:キャッシュを作る“加速器”
- AIエージェントSaaS(中小のバックオフィス丸ごと代行)を国内→英語圏へ展開。
- バーティカル選定:EC事業者、クリエイター、越境D2C、予約業(美容/医療)。
- 目標:MRR10億円、解約率<2%/月、粗利>80%。
1〜3年:単位経済の怪物化
- ロボ×サブスクを倉庫/小売→介護へ横展開。稼働率と保守コストをAI最適化。
- 決済APIをSaaSに同梱(AR/AP自動化→即時決済)。TPV10兆円を狙う。
- グロース資金:コンバーチブル+プロジェクトファイナンス。
3〜7年:インフラに跨る
- エネルギー(再エネ+蓄電+PPA)を地域ごとにSPV組成し連結。
- 半導体は先端パッケージと専用チップに集中投資(水平分業でリスク軽減)。
- 目標:連結売上3〜5兆円、EBITDA 0.6〜1兆円レベル。
7〜15年:ネットワーク支配
- 決済網・物流網・SaaS網を相互補完させスイッチングコストを極大化。
- コングロマリット・ディスカウントを潰すため一部事業を分割上場。
- 目標:時価総額100〜300兆円帯へ。
15年以降:1000兆円レンジ
- 国家級インフラ(エネ・半導体・物流)+GAFA級AIプラットフォームの二階建て。
- 買収+合弁で“最後の数百兆”を埋める(年金・SWFと共同)。
資本政策(ざっくり)
- SaaS/決済:増資<レバレッジ。ユニット経済改善で負債コスト<ROICの状態を維持。
- エネルギー/物流/半導体:プロジェクトファイナンス、グリーン/トランジションボンド、補助金・税控除を最大活用。
- M&A:EBITDAマルチプル<自社マルチプルの時期に株式対価中心で。
リスク&対策
- 規制:決済/医療/エネルギーは先に法務を内製。政策連携チーム必須。
- 資材/金利:長期固定の調達、サプライヤー分散、在庫と先物のヘッジ。
- 技術陳腐化:コアはAPI化し常に差し替え可能に。買う/作るを冷徹に分離。
いまからの“実装”プラン(90日)
- 1業界特化AIエージェントを即ローンチ
- 例:EC運営の商品登録→在庫→CS→請求→会計を自動化。
- 価格:1社 月3〜5万円。1000社で月3〜5億円。
- 内蔵決済:請求→入金までワンストップ(手数料で薄利多売)。
- ロボ実証:既存AGV/AMRを月額化して1〜2現場で回す(事例と動画を武器に拡販)。
- 資金:顧客前受金+リボルビング+補助金申請。株式は“良いバリュエーションが出た時だけ”。
ミニ“お金の方程式”集(目安)
- AIバックオフィス:5万社×月5万円=年300億円(粗利>80%)
- ロボ×サブスク:1万台×年200万円=年200億円(スケールで整備費↓)
- 決済:TPV10兆円×0.25%=年250億円
- 合計:まず年数百億円→年数千億円→大型インフラで年数兆円の順。
まとめ
- 1000兆円は**“複数の産業を横断するエコシステム”**を持つことでのみ到達。
- 入口はAI×決済で“軽く速く”作り、重いインフラへ資金の幹を通す。
- 「何からやるか」は一業界特化AIエージェントが最短のキャッシュ源です。
もしよければ、あなたの今の強み(Web/ゲーム/AI)に合わせて**“EC運営自動化エージェント(日本語特化)”の初期仕様と料金表**、営業スクリプト、LP文面までこの場で作ります。すぐ行けます。
VRMMORPGβ版
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>エルダークロニクル:VR超完全版 – ロードなし(拾う/装備修正)</title>
<script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script>
<style>
body, html { margin:0; padding:0; overflow:hidden; font-family:'Yu Gothic',system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:#000;}
#ui {
position:absolute; top:20px; left:20px; z-index:10; width:400px; color:#fff;
background:linear-gradient(180deg,rgba(0,0,0,.92),rgba(0,10,30,.85));
border-radius:16px; padding:14px 16px; box-shadow:0 0 25px #0ff,0 0 18px #33f4; font-size:15px;
}
h2 { margin:0 0 8px 0; font-size:22px; letter-spacing:1px;}
.row { display:flex; gap:8px; align-items:center; flex-wrap:wrap;}
.row > * { flex: 1 1 auto; }
input, select { width:100%; padding:6px 8px; border-radius:8px; border:1px solid #024; background:#00131f; color:#cfe9ff; }
button {
background:linear-gradient(90deg,#555,#222 80%);
color:#fff; padding:10px; margin:5px 0; border:none; cursor:pointer; width:100%;
border-radius:8px; font-weight:bold; letter-spacing:1px; box-shadow:0 1px 8px #0cf5; transition:background .25s;
}
button:hover { background:linear-gradient(90deg,#888 40%,#3cf); }
.section-title { font-weight:bold; font-size:17px; margin-top:10px; border-bottom:2px solid #5ef; letter-spacing:1.2px; }
.bar { height:14px; background:#13202a; border-radius:7px; overflow:hidden; margin:6px 0; box-shadow:0 1px 6px #0ff6 inset; }
.bar-inner { height:100%; background:linear-gradient(90deg,#0f0,#3af); transition:width .25s; }
.mana-bar .bar-inner { background:linear-gradient(90deg,#33d,#6ff); }
.enemyhp-bar .bar-inner { background:linear-gradient(90deg,#f55,#fdd); }
#scenario { margin:10px 0 6px 0; background:rgba(0,10,32,0.7); padding:8px 12px; border-radius:8px; min-height:44px; }
.kbd { display:inline-block; padding:1px 6px; border-radius:6px; background:#0a2636; border:1px solid #124; font-family:monospace; }
#hint {
position:absolute; bottom:18px; left:50%; transform:translateX(-50%);
color:#eaffff; background:rgba(0,20,35,.72); border:1px solid #0af; padding:8px 12px; border-radius:10px;
box-shadow:0 0 18px #08f5; font-size:14px; z-index:10;
}
#pickupPrompt {
position:absolute; bottom:70px; left:50%; transform:translateX(-50%);
color:#fff; background:rgba(0,0,0,.7); border:1px solid #38f; padding:8px 12px; border-radius:10px;
display:none; z-index:10;
}
.tag { display:inline-block; padding:2px 6px; border-radius:6px; background:#012233; border:1px solid #1a4e6c; margin-left:6px; font-size:12px; color:#bfe6ff;}
.on { background:#0b3754; border-color:#3db3ff; color:#fff; }
</style>
</head>
<body>
<div id="ui">
<h2>エルダークロニクルVR</h2>
<div class="row">
<span>プレイヤー名:</span>
<input id="playerName" placeholder="名前を入力">
</div>
<div class="row"><span>レベル:</span><span id="level" class="tag on">1</span>
<span>装備:</span><span id="equipment" class="tag">なし</span>
</div>
<div>HP: <span id="hpText">100</span></div>
<div class="bar"><div id="hpBar" class="bar-inner" style="width:100%"></div></div>
<div>魔力: <span id="manaText">100</span></div>
<div class="bar mana-bar"><div id="manaBar" class="bar-inner" style="width:100%"></div></div>
<div>敵HP: <span id="enemyHpText">120</span></div>
<div class="bar enemyhp-bar"><div id="enemyHpBar" class="bar-inner" style="width:100%"></div></div>
<div class="section-title">シナリオ</div>
<div id="scenario"></div>
<div class="section-title">行動</div>
<button onclick="levelUp()">🎉 レベルアップ</button>
<button onclick="castSpell()">🪄 魔法発動</button>
<button onclick="choosePath('wizard')">🧙♂️ 魔導士に話す</button>
<button onclick="choosePath('knight')">🛡️ 騎士に話す</button>
<button onclick="receiveQuest()">📜 クエスト受注</button>
<button onclick="toggleEnvironment()">🌄 昼夜切替</button>
<div class="section-title">移動</div>
<div class="row">
<button onclick="changeField('town')">🏘️ 街</button>
<button onclick="changeField('castle')">🏰 城</button>
</div>
<div class="row">
<button onclick="changeField('cave')">🕳️ 洞窟</button>
<button onclick="changeField('ruins')">🏛️ 遺跡</button>
</div>
<div class="row">
<button onclick="changeField('dungeon')">🧩 ダンジョン</button>
</div>
<div class="section-title">操作</div>
<div style="line-height:1.6">
<span class="kbd">WASD</span> 移動
<span class="kbd">マウス</span> 視点
<span class="kbd">E</span> 拾う
<span class="kbd">1</span> 剣装備
<span class="kbd">2</span> 銃装備
<span class="kbd">クリック</span> 攻撃
</div>
</div>
<div id="pickupPrompt">Eで拾う</div>
<div id="hint">近くの武器に近づいて <span class="kbd">E</span> で拾い、<span class="kbd">1/2</span> で装備、クリックで攻撃!</div>
<a-scene loading-screen="enabled:false" renderer="colorManagement:true" shadow="true">
<a-sky id="sky" color="#0e163e"></a-sky>
<a-entity id="starParticles" position="0 25 -40" visible="false">
<a-entity geometry="primitive:sphere; radius:0.18" material="color:#fff; opacity:0.6" position="-10 3 0"></a-entity>
<a-entity geometry="primitive:sphere; radius:0.12" material="color:#fff; opacity:0.8" position="7 2 -2"></a-entity>
<a-entity geometry="primitive:sphere; radius:0.11" material="color:#eaf6ff; opacity:0.9" position="3 5 2"></a-entity>
<a-entity geometry="primitive:sphere; radius:0.10" material="color:#ffe; opacity:0.7" position="13 3 5"></a-entity>
</a-entity>
<a-entity id="cloudParticles" position="0 30 -35" visible="true">
<a-sphere radius="5" position="8 2 -8" color="#f6fbff" opacity="0.18"></a-sphere>
<a-sphere radius="6" position="-7 3 5" color="#eefbff" opacity="0.14"></a-sphere>
</a-entity>
<a-light type="ambient" color="#fff" intensity="1"></a-light>
<a-light id="sunlight" type="directional" intensity="1.6" position="20 25 -8" castShadow="true" shadow-mapWidth="1024" shadow-mapHeight="1024"></a-light>
<a-light type="point" color="#cff" intensity="2.2" distance="50" position="2 9 -3"></a-light>
<a-light type="spot" color="#55aaff" position="0 15 -10" intensity="1.2" angle="30" penumbra="0.7"></a-light>
<a-plane id="ground" position="0 0 0" rotation="-90 0 0" width="120" height="120" color="#375047" shadow="receive:true"></a-plane>
<a-entity id="field-town" visible="true">
<a-circle position="0 0 -6" radius="4" color="#7d7d7d" material="roughness:.9; metalness:.05; opacity:.84; transparent:true"></a-circle>
<a-entity position="-2 0 -7">
<a-box width="3.6" height="2.6" depth="2.4" color="#c0a47b" material="roughness:0.3; metalness:0.05" position="0 1.3 0"></a-box>
<a-cone position="0 3 -0.1" radius-bottom="2.1" height="1.4" color="#7b5322"></a-cone>
<a-text value="街の家" position="0 3.7 0" color="#fff" width="5" align="center"></a-text>
</a-entity>
<a-entity position="2 0 -9">
<a-box width="2.2" height="2.2" depth="2.4" color="#a86f23" position="0 1.1 0"></a-box>
<a-cone position="0 2.6 0" radius-bottom="1.4" height="1.0" color="#5c3a12"></a-cone>
</a-entity>
<a-entity position="5 0 -5">
<a-cylinder radius="0.25" height="4" color="#3a2a1a" position="0 2 0"></a-cylinder>
<a-sphere radius="1.2" color="#174d1f" position="0 3.2 0"></a-sphere>
</a-entity>
</a-entity>
<a-entity id="field-castle" visible="false">
<a-box position="0 2.8 -12" depth="6" height="6" width="10" color="#ccd2df" material="roughness:0.25; metalness:0.12"></a-box>
<a-cylinder position="-4 1.2 -13" radius="1.1" height="3.8" color="#858ca3"></a-cylinder>
<a-cylinder position="4 1.2 -13" radius="1.1" height="3.8" color="#858ca3"></a-cylinder>
<a-cone position="0 6.4 -12" radius-bottom="3.2" height="2" color="#dba"></a-cone>
<a-entity geometry="primitive:torus; radius:2.5; tube:0.07" material="color:#66aaff; opacity:.25; transparent:true" position="0 4.5 -12"></a-entity>
<a-text value="王の城" position="0 6.3 -12" color="#0bf" width="6" align="center"></a-text>
</a-entity>
<a-entity id="field-cave" visible="false">
<a-torus position="2 1.1 -10" radius="1.8" tube="0.8" arc="230" color="#363636" rotation="40 0 90"></a-torus>
<a-sphere position="2 0.4 -10" radius="1.1" color="#151515"></a-sphere>
<a-entity position="-2 0 -8">
<a-cone radius-bottom="0.8" height="2.2" color="#2a2a2a"></a-cone>
<a-cone radius-bottom="0.5" height="1.4" color="#393939" position="0.7 0 0.4"></a-cone>
</a-entity>
<a-text value="洞窟の入口" position="2 2.9 -10" color="#fff" width="6" align="center"></a-text>
</a-entity>
<a-entity id="field-ruins" visible="false">
<a-cylinder position="-2 1.1 -9" radius="0.8" height="2.6" color="#babbb2"></a-cylinder>
<a-box position="-3.1 1.8 -9" width="3.5" height="0.32" depth="0.7" color="#e0dfc7"></a-box>
<a-torus position="-2 2.3 -9" radius="0.6" tube="0.09" arc="340" color="#fffeee"></a-torus>
<a-entity geometry="primitive:torusKnot; p:2; q:7; radius:0.7; tube:0.07" position="2 1.8 -8" material="color:#bcd; opacity:.7; transparent:true"></a-entity>
<a-text value="古代の遺構" position="0 3.3 -8.5" color="#fff" width="6" align="center"></a-text>
</a-entity>
<a-entity id="field-dungeon" visible="false">
<a-box position="2 1.2 -8.8" depth="3.3" height="2.3" width="3.6" color="#161651"></a-box>
<a-torus-knot position="-1.5 2.7 -8.2" radius="0.9" tube="0.12" p="3" q="7" color="#64eaff"></a-torus-knot>
<a-cylinder position="-3 0.7 -9.6" radius="0.8" height="1.2" color="#333"></a-cylinder>
<a-entity geometry="primitive:torus; radius:1.4; tube:0.05" material="color:#aaeeff; opacity:.18; transparent:true" position="0 2.1 -8.6" rotation="90 0 0"></a-entity>
<a-text value="ダンジョン入口" position="0 3.1 -8.4" color="#fff" width="6" align="center"></a-text>
</a-entity>
<!-- 武器ピックアップ -->
<a-entity id="swordPickup" class="pickup" data-weapon="sword" position="-1 0 -5">
<a-box width="0.16" height="1.3" depth="0.08" color="#cfe7ff" material="metalness:0.8; roughness:0.15" position="0 0.75 0"></a-box>
<a-box width="0.5" height="0.08" depth="0.08" color="#333" position="0 0.1 0"></a-box>
<a-cylinder radius="0.06" height="0.42" color="#7c5a2b" position="0 -0.1 0"></a-cylinder>
<a-entity geometry="primitive:ring; radiusInner:0.45; radiusOuter:0.5" rotation="-90 0 0" position="0 0 0" material="color:#44d; opacity:.35; transparent:true"></a-entity>
<a-text value="剣" color="#fff" position="0 1.7 0" align="center"></a-text>
</a-entity>
<a-entity id="gunPickup" class="pickup" data-weapon="gun" position="2 0 -5.5">
<a-box width="0.7" height="0.18" depth="0.18" color="#222" position="0 0.4 0"></a-box>
<a-box width="0.3" height="0.28" depth="0.18" color="#444" position="-0.2 0.2 0"></a-box>
<a-box width="0.12" height="0.40" depth="0.16" color="#333" position="0.2 0.15 0"></a-box>
<a-cylinder radius="0.06" height="0.28" color="#555" position="0.34 0.46 0" rotation="0 0 90"></a-cylinder>
<a-entity geometry="primitive:ring; radiusInner:0.45; radiusOuter:0.5" rotation="-90 0 0" position="0 0 0" material="color:#0aa; opacity:.35; transparent:true"></a-entity>
<a-text value="銃" color="#fff" position="0 1.0 0" align="center"></a-text>
</a-entity>
<!-- プレイヤー(リグが移動する:★修正) -->
<a-entity id="rig" position="0 0 0" wasd-controls="acceleration:30">
<a-entity id="cam" camera look-controls="pointerLockEnabled:true" position="0 1.6 3"></a-entity>
<a-entity id="player" position="0 0 -1.5" rotation="0 180 0" shadow="cast:true">
<a-sphere radius="0.18" color="#ffd7f0" position="0 1.58 0"></a-sphere>
<a-cylinder radius="0.23" height="0.9" color="#9ad" position="0 1.02 0"></a-cylinder>
<a-cylinder radius="0.08" height="0.55" color="#9ad" position="-0.16 0.74 0" rotation="0 0 18"></a-cylinder>
<a-cylinder radius="0.08" height="0.55" color="#9ad" position="0.16 0.74 0" rotation="0 0 -18"></a-cylinder>
<a-cylinder radius="0.09" height="0.7" color="#79b" position="-0.10 0.35 0"></a-cylinder>
<a-cylinder radius="0.09" height="0.7" color="#79b" position="0.10 0.35 0"></a-cylinder>
</a-entity>
</a-entity>
<!-- 敵 -->
<a-entity id="enemy" position="0 0 -9" visible="true">
<a-sphere id="enemyBody" radius="1.25" color="#9b1e1e" material="metalness:0.35; roughness:.25; emissive:#330000"></a-sphere>
<a-entity geometry="primitive:torus; radius:1.4; tube:0.05" material="color:#ffeeaa; opacity:.18; transparent:true" position="0 0.3 0" rotation="90 0 0"></a-entity>
<a-entity geometry="primitive:torusKnot; radius:0.45; tube:0.05; p:2; q:5" material="color:#ffa033; opacity:.22; transparent:true" position="0 1.0 0"></a-entity>
<a-sphere position="-0.38 0.42 0.95" radius="0.14" color="#fff" material="metalness:.9; roughness:.05; emissive:#a0f"></a-sphere>
<a-sphere position="0.38 0.42 0.95" radius="0.14" color="#fff" material="metalness:.9; roughness:.05; emissive:#a0f"></a-sphere>
<a-cone position="-0.45 1.35 0.15" radius-bottom="0.16" height="0.62" color="#f5f3ff" rotation="15 0 60"></a-cone>
<a-cone position="0.45 1.35 0.15" radius-bottom="0.16" height="0.62" color="#f5f3ff" rotation="15 0 -60"></a-cone>
<a-text value="敵モンスター" position="0 2.15 0" align="center" color="#fff"></a-text>
</a-entity>
<a-entity id="spellEffect" geometry="primitive:sphere; radius:0.55"
material="color:#72f3ff; opacity:.86; emissive:#88f; transparent:true"
position="0 1.7 -3" visible="false"
animation__move="property: position; to: 0 3.1 -6; dur:500; dir:alternate; loop:false">
<a-entity geometry="primitive:torus; radius:0.7; tube:0.08" material="color:#fff; opacity:.3; transparent:true"></a-entity>
</a-entity>
<a-entity id="game" game-manager></a-entity>
</a-scene>
<script>
let isDay=false, hp=100, mana=100, level=1;
let enemyHP=120, enemyHPMax=120;
function updateBars(){
document.getElementById("hpText").innerText = Math.max(0,Math.floor(hp));
document.getElementById("manaText").innerText = Math.max(0,Math.floor(mana));
document.getElementById("hpBar").style.width = Math.max(0,Math.min(100,hp))+"%";
document.getElementById("manaBar").style.width = Math.max(0,Math.min(100,mana))+"%";
document.getElementById("level").innerText = level;
document.getElementById("enemyHpText").innerText = Math.max(0,enemyHP);
const w = Math.max(0, Math.min(100, enemyHP*100/enemyHPMax));
document.getElementById("enemyHpBar").style.width = w+"%";
}
function levelUp(){ level++; hp=Math.min(100,hp+20); mana+=30; showScenario("🎉 レベルアップ!新しい力が湧いてくる!"); updateBars(); }
function castSpell(){
if(mana<20){ showScenario("💤 魔力が足りない!"); return; }
mana-=20; updateBars();
const effect=document.getElementById("spellEffect");
effect.setAttribute("visible","true");
setTimeout(()=>{ effect.setAttribute("visible","false"); applyDamageToEnemy(30,"魔法ヒット"); },700);
}
function choosePath(choice){
if(choice==="wizard"){ document.getElementById("equipment").innerText="魔導士のローブ"; mana+=50; showScenario("🧙♂️ 魔導士の試練が始まる…"); }
else{ document.getElementById("equipment").innerText="騎士の剣"; hp+=30; showScenario("⚔️ 騎士と共に魔王城へ向かう!"); }
updateBars();
}
function toggleEnvironment(){
const sky=document.getElementById("sky");
const star=document.getElementById("starParticles");
const cloud=document.getElementById("cloudParticles");
const ground=document.getElementById("ground");
const sunlight=document.getElementById("sunlight");
if(isDay){
sky.setAttribute("color","#0e163e"); star.setAttribute("visible","false"); cloud.setAttribute("visible","true");
ground.setAttribute("color","#375047"); sunlight.setAttribute("intensity","1.6"); showScenario("🌌 夜の世界へ…");
}else{
sky.setAttribute("color","#7ddfff"); star.setAttribute("visible","true"); cloud.setAttribute("visible","false");
ground.setAttribute("color","#b8ffcc"); sunlight.setAttribute("intensity","2.1"); showScenario("🌞 昼の世界へ…");
}
isDay=!isDay;
}
function changeField(fieldName){
const fields=['town','castle','cave','ruins','dungeon'];
fields.forEach(name=>{
const el=document.getElementById(`field-${name}`);
el.setAttribute('visible', name===fieldName);
});
showScenario(`📍 ${fieldName} に移動しました`);
}
function receiveQuest(){
const quests=[
"魔導士の塔で失われた書を探せ!","騎士団の旗を取り戻せ!","洞窟の奥に眠る魔石を発見せよ!",
"遺跡に隠された封印を解け!","ダンジョンの魔王を討伐せよ!"
];
const index=Math.floor(Math.random()*quests.length);
showScenario("📜 クエスト受注: "+quests[index]);
}
const scenarioList=[
"目覚めたあなたは不思議な世界にいた。","最初のクエストを受注しよう。","フィールド移動で冒険の扉が開く。",
"行動や魔法でストーリーが動く。","街で情報を集め、仲間と出会おう。","クエストを進め、魔王に立ち向かえ!"
];
let scenarioIndex=0;
function showScenario(text){ document.getElementById('scenario').innerHTML=text; }
function advanceScenario(){ if(scenarioIndex<scenarioList.length){ showScenario(scenarioList[scenarioIndex]); scenarioIndex++; } }
['levelUp','castSpell','choosePath','changeField','receiveQuest'].forEach(fn=>{
const orig=window[fn]; window[fn]=function(){ orig.apply(this, arguments); advanceScenario(); }
});
/* ---- ここが拾う/装備の中核 ---- */
const rigEl=()=>document.getElementById('rig');
const camEl=()=>document.getElementById('cam');
const enemyEl=()=>document.getElementById('enemy');
const enemyBodyEl=()=>document.getElementById('enemyBody');
let hasSword=false, hasGun=false, equipped='none';
let attackCooldown=false;
const bullets=[];
function setEquipmentLabel(){
let label="なし";
if(equipped==='sword') label="剣";
if(equipped==='gun') label="銃";
document.getElementById('equipment').innerText=label;
}
function tryPickup(){
const rpos=rigEl().object3D.position; // ★リグが移動するのでOK
const items=[document.getElementById('swordPickup'),document.getElementById('gunPickup')];
for(const it of items){
if(!it.getAttribute('visible')) continue;
const wpos=it.object3D.position;
const d=rpos.distanceTo(wpos);
if(d<2.0){
const w=it.getAttribute('data-weapon');
it.setAttribute('visible','false');
if(w==='sword'){ hasSword=true; if(equipped==='none') equipped='sword'; showScenario("🗡️ 剣を拾った! 1で装備。"); }
if(w==='gun'){ hasGun=true; if(equipped==='none') equipped='gun'; showScenario("🔫 銃を拾った! 2で装備。"); }
setEquipmentLabel();
return;
}
}
showScenario("近くに拾えるものはない。");
}
function equipSword(){ if(hasSword){ equipped='sword'; setEquipmentLabel(); showScenario("🗡️ 剣を装備した。"); } else { showScenario("剣をまだ拾っていない。"); } }
function equipGun(){ if(hasGun){ equipped='gun'; setEquipmentLabel(); showScenario("🔫 銃を装備した。"); } else { showScenario("銃をまだ拾っていない。"); } }
function attack(){
if(attackCooldown) return;
if(equipped==='sword'){ swordSlash(); }
else if(equipped==='gun'){ shootBullet(); }
else{ showScenario("装備がありません。剣や銃を拾ってください。"); }
}
function swordSlash(){
attackCooldown=true;
const slash=document.createElement('a-entity');
slash.setAttribute('geometry','primitive: torus; radius:0.9; tube:0.08; arc:200');
slash.setAttribute('material','color:#fff; opacity:.6; transparent:true');
const dir=new AFRAME.THREE.Vector3(); camEl().object3D.getWorldDirection(dir); dir.y=0; dir.normalize();
const base=rigEl().object3D.position.clone().add(new AFRAME.THREE.Vector3(0,1.2,0)).add(dir.clone().multiplyScalar(0.8));
slash.setAttribute('position',`${base.x} ${base.y} ${base.z}`);
const rotY=Math.atan2(dir.x,dir.z)*AFRAME.THREE.MathUtils.RAD2DEG;
slash.setAttribute('rotation',`0 ${rotY} 0`);
slash.setAttribute('animation__fade','property: material.opacity; to:0; dur:200; easing:easeOutQuad');
document.querySelector('a-scene').appendChild(slash);
setTimeout(()=>slash.parentNode && slash.parentNode.removeChild(slash),220);
const enemyPos=enemyEl().object3D.position.clone();
const toEnemy=enemyPos.clone().sub(rigEl().object3D.position.clone().add(new AFRAME.THREE.Vector3(0,1.0,0)));
const dist=toEnemy.length();
toEnemy.y=0; toEnemy.normalize();
const forward=dir.clone();
const angle=forward.dot(toEnemy);
if(dist<2.6 && angle>0.5 && enemyEl().getAttribute('visible')){
applyDamageToEnemy(25,"斬撃");
} else {
showScenario("空振り…");
}
setTimeout(()=>attackCooldown=false,280);
}
function shootBullet(){
attackCooldown=true;
const bullet=document.createElement('a-sphere');
bullet.setAttribute('radius','0.07');
bullet.setAttribute('color','#e6f7ff');
bullet.setAttribute('material','emissive:#88d; metalness:.6; roughness:.2');
const dir=new AFRAME.THREE.Vector3(); camEl().object3D.getWorldDirection(dir); dir.normalize();
const start=rigEl().object3D.position.clone().add(new AFRAME.THREE.Vector3(0,1.3,0)).add(dir.clone().multiplyScalar(0.9));
bullet.setAttribute('position',`${start.x} ${start.y} ${start.z}`);
document.querySelector('a-scene').appendChild(bullet);
bullets.push({el:bullet, dir:dir.clone(), life:1800, speed:24});
setTimeout(()=>attackCooldown=false,120);
}
function applyDamageToEnemy(dmg,label){
if(!enemyEl().getAttribute('visible')) return;
enemyHP=Math.max(0, enemyHP - dmg);
updateBars();
const origColor=enemyBodyEl().getAttribute('color');
enemyBodyEl().setAttribute('color','#ffffff');
setTimeout(()=>enemyBodyEl().setAttribute('color', origColor), 80);
spawnDamageText(dmg,label);
if(enemyHP<=0){ killEnemy(); }
}
function spawnDamageText(dmg,label){
const t=document.createElement('a-text');
t.setAttribute('value',`${label} -${dmg}`);
t.setAttribute('color','#ffe6e6');
t.setAttribute('align','center');
const p=enemyEl().object3D.position.clone();
t.setAttribute('position',`${p.x} ${p.y+2.4} ${p.z}`);
t.setAttribute('animation__rise','property: position; to: '+`${p.x} ${p.y+3.2} ${p.z}`+'; dur:650; easing:easeOutQuad');
t.setAttribute('animation__fade','property: opacity; to:0; dur:650; easing:easeOutQuad');
document.querySelector('a-scene').appendChild(t);
setTimeout(()=>t.parentNode && t.parentNode.removeChild(t),700);
}
function killEnemy(){
enemyEl().setAttribute('visible','false');
showScenario("✅ 敵を倒した! 5秒後に再出現する…");
setTimeout(()=>respawnEnemy(), 5000);
}
function respawnEnemy(){
enemyHP=enemyHPMax; updateBars();
const e=enemyEl();
const x = (Math.random()*6 - 3);
const z = -8.5 + (Math.random()*3 - 1.5);
e.setAttribute('position',`${x} 0 ${z}`);
e.setAttribute('visible','true');
showScenario("⚠️ 新たな敵が現れた!");
}
/* 入力(E/1/2 の互換強化:★修正) */
AFRAME.registerComponent('game-manager',{
init:function(){
window.addEventListener('keydown',(e)=>{
const k=e.key;
const c=e.code;
if(c==='KeyE' || k==='e' || k==='E') tryPickup();
if(c==='Digit1' || c==='Numpad1' || k==='1') equipSword();
if(c==='Digit2' || c==='Numpad2' || k==='2') equipGun();
});
window.addEventListener('mousedown',()=>attack());
updateBars(); advanceScenario(); setEquipmentLabel();
},
tick:function(time,dt){
const delta=dt/1000;
const prompt=document.getElementById('pickupPrompt');
const rpos=rigEl().object3D.position;
let near=false;
['swordPickup','gunPickup'].forEach(id=>{
const el=document.getElementById(id);
if(el.getAttribute('visible')){
const d=rpos.distanceTo(el.object3D.position);
if(d<2.0) near=true;
}
});
prompt.style.display = near?'block':'none';
// 弾
for(let i=bullets.length-1;i>=0;i--){
const b=bullets[i];
if(!b.el.parentNode){ bullets.splice(i,1); continue; }
b.life -= dt;
const pos=b.el.object3D.position;
pos.add(b.dir.clone().multiplyScalar(24*delta));
if(b.life<=0){ b.el.parentNode.removeChild(b.el); bullets.splice(i,1); continue; }
if(enemyEl().getAttribute('visible')){
const d=pos.distanceTo(enemyEl().object3D.position);
if(d<1.35){
applyDamageToEnemy(15,"射撃");
b.el.parentNode.removeChild(b.el);
bullets.splice(i,1);
}
}
}
enemyEl().object3D.rotation.y += delta*0.3;
}
});
updateBars();
advanceScenario();
</script>
</body>
</html>
GPT-5
GPT‑5(正式には次世代ChatGPTとしてリリース予定)は、無料プランでも利用可能になる予定で、ユーザーはいわゆる標準的なインテリジェンス設定で無制限にチャット利用できるとOpenAI CEOのサム・アルトマン氏が公式に発表しています。 Plus や Pro の有料プランでは、さらに高度な知能レベルや追加機能にアクセスできるようになります note(ノート)+7note(ノート)+7MiraLab.inc+7。
🎯 どこで無料で使えるか?
- ChatGPT(OpenAI公式ウェブ版/アプリ)
無料アカウントでも GPT‑5(標準モード)を利用可能。乱用防止のための使用制限(例:1日の回数上限など)は設けられる可能性があるが、無制限チャットが基本です Reddit。 - Microsoft Copilot
Windows PCに統合された Copilot は GPT‑5 に対応する「Smart モード」を導入予定で、プラットフォームそのもので GPT‑5 を無料で利用できるようになると見られます(ただし、精度やレスポンスの仕様は流動的)note(ノート)+4note(ノート)+4accelc.co.jp+4。
📋 まとめ
| サービス | GPT‑5 利用状況(無料プラン) |
|---|---|
| ChatGPT(OpenAI公式) | ✅ 無料で標準インテリジェンス設定において無制限利用可能(ただし乱用対策あり) |
| Microsoft Copilot(Windows統合) | ✅ Smart モードで GPT‑5 を採用、無料での使用が期待される |
| その他のAIサービス | 現時点では GPT‑5 の対応情報は限定的 |
ℹ️ 注意点と補足
- GPT‑5 は現在、2025年8月ごろの正式リリースが予定されていますThe Verge。したがって、ChatGPT の無料版でも GPT‑5 を使い始めるのはリリース後になります。
- アルトマン氏は「地球上のすべての人へのGPT‑5の無料提供」を理想として語っていますが、これはまだ公式に実現しているわけではなく、長期的な構想です chatgpt5free.com+15note(ノート)+15shift-ai.co.jp+15。
- 無料プランでは乱用防止や公平性維持のために、トークン制限・アクセス制限などの利用制約が設けられる可能性があります。また、Plus/Pro プランでは優先アクセスや高度な機能が得られます MiraLab.inc+1note(ノート)+1。
✅ ご参考まで
- 使い始めるには?
OpenAI公式サイトまたはモバイルアプリから ChatGPT にサインアップ・ログインすれば、無料プランで GPT‑5(標準モード)が利用可能になります(リリース後)。 - Microsoft Copilot を使うなら?
Windows PC に標準搭載されている Copilot を通じて、GPT‑5 Smart モードで利用できるようになる予定です。
まとめると、GPT‑5 はリリース後に ChatGPT の無料プランで使えます(標準モードで無制限利用、ただし制限あり)。さらに、Microsoft Copilot ユーザーも同時に利用できる可能性があるとの見通しです。
推しログ – 推し活情報共有サービス
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>推しログ - 推し活情報共有サービス</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="bg-gradient-to-r from-purple-200 to-indigo-200">
<!-- ヘッダー -->
<header class="bg-purple-600 text-white p-4 shadow-lg">
<div class="container mx-auto flex justify-between items-center">
<h1 class="text-3xl font-bold"><i class="fa-solid fa-heart mr-2"></i>推しログ</h1>
<nav class="space-x-4">
<a href="#" class="hover:text-gray-200">ホーム</a>
<a href="#" class="hover:text-gray-200">スケジュール</a>
<a href="#" class="hover:text-gray-200">ニュース</a>
<a href="#" class="hover:text-gray-200">ログイン</a>
</nav>
</div>
</header>
<!-- メインコンテンツ -->
<main class="container mx-auto my-8 px-4">
<!-- 推しスケジュール -->
<section class="bg-white p-8 rounded-xl shadow-xl mb-8">
<h2 class="text-2xl font-bold border-b pb-2 mb-4"><i class="fa-regular fa-calendar mr-2"></i>今月の推しスケジュール</h2>
<ul class="space-y-3">
<li class="flex justify-between items-center p-4 bg-gray-50 rounded-lg shadow">
<span class="font-semibold">ライブ配信「推しの部屋」</span>
<span class="text-gray-500">8/10(木) 20:00〜</span>
</li>
<li class="flex justify-between items-center p-4 bg-gray-50 rounded-lg shadow">
<span class="font-semibold">ニューシングル発売日!</span>
<span class="text-gray-500">8/15(火)</span>
</li>
<li class="flex justify-between items-center p-4 bg-gray-50 rounded-lg shadow">
<span class="font-semibold">ファンクラブ限定イベント</span>
<span class="text-gray-500">8/25(金) 18:30〜</span>
</li>
</ul>
<button class="mt-4 bg-purple-600 hover:bg-purple-700 text-white py-2 px-4 rounded">もっと見る</button>
</section>
<!-- 最新推しニュース -->
<section class="bg-white p-8 rounded-xl shadow-xl mb-8">
<h2 class="text-2xl font-bold border-b pb-2 mb-4"><i class="fa-solid fa-newspaper mr-2"></i>最新推しニュース</h2>
<article class="mb-6 border-b pb-4">
<h3 class="font-semibold text-purple-700 mb-1">推しの新曲MVが公開!</h3>
<p class="text-gray-600">待望の新曲MVが公式YouTubeチャンネルにて公開されました!視聴数も急上昇中!</p>
<a href="#" class="text-blue-500 hover:underline">詳しく見る <i class="fa-solid fa-arrow-right ml-1"></i></a>
</article>
<article>
<h3 class="font-semibold text-purple-700 mb-1">推し、テレビ番組に出演決定!</h3>
<p class="text-gray-600">8/12(土)の「音楽バズ」にゲスト出演予定です。特別トークもお楽しみに!</p>
<a href="#" class="text-blue-500 hover:underline">詳しく見る <i class="fa-solid fa-arrow-right ml-1"></i></a>
</article>
<button class="mt-4 bg-purple-600 hover:bg-purple-700 text-white py-2 px-4 rounded">もっと見る</button>
</section>
<!-- 推しメンバー紹介 -->
<section class="bg-white p-8 rounded-xl shadow-xl">
<h2 class="text-2xl font-bold border-b pb-2 mb-4"><i class="fa-solid fa-user-group mr-2"></i>推しメンバー紹介</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="text-center">
<img src="https://via.placeholder.com/150" class="rounded-full mx-auto mb-2" alt="推し1">
<h3 class="font-semibold">推しメン1</h3>
</div>
<div class="text-center">
<img src="https://via.placeholder.com/150" class="rounded-full mx-auto mb-2" alt="推し2">
<h3 class="font-semibold">推しメン2</h3>
</div>
<div class="text-center">
<img src="https://via.placeholder.com/150" class="rounded-full mx-auto mb-2" alt="推し3">
<h3 class="font-semibold">推しメン3</h3>
</div>
<div class="text-center">
<img src="https://via.placeholder.com/150" class="rounded-full mx-auto mb-2" alt="推し4">
<h3 class="font-semibold">推しメン4</h3>
</div>
</div>
</section>
</main>
<!-- フッター -->
<footer class="bg-gray-900 text-white text-center py-4 mt-8">
<p class="text-sm">© 2024 推しログ. All rights reserved.</p>
</footer>
</body>
</html>
