<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verse - 次世代ソーシャルネットワーク</title>
<!-- SVG ファビコンを指定 -->
<link rel="shortcut icon" href="favicon.ico">
<!-- PNG版 -->
<link rel="icon" type="image/png" href="favicon.png">
<!-- SVG版 -->
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/rita@3.0.0/dist/rita.min.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.glass-effect {
background: rgba(255,255,255,0.25);
backdrop-filter: blur(10px);
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
}
.card-hover { transition: all 0.3s ease; }
.card-hover:hover {
transform: translateY(-5px);
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04);
}
.gradient-text {
background: linear-gradient(45deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.timeline-post {
background: white;
border-radius: 15px;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
transition: all 0.3s ease;
border-left: 4px solid #667eea;
}
.timeline-post:hover {
transform: translateX(5px);
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
}
.profile-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
border: 4px solid white;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.btn-primary {
background: linear-gradient(45deg, #667eea, #764ba2);
border: none;
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 7px 14px rgba(0,0,0,0.18);
}
.section-divider {
height: 3px;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 2px;
margin: 2rem 0;
}
.username-badge {
background: linear-gradient(45deg, #667eea, #764ba2);
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: 5px;
}
.status-active {
background-color: #10b981;
animation: pulse 2s infinite;
}
.status-inactive { background-color: #6b7280; }
.log-container {
max-height: 150px;
overflow-y: auto;
background: rgba(255,255,255,0.1);
border-radius: 5px;
padding: 10px;
margin-top: 10px;
font-size: 0.8rem;
font-family: monospace;
}
.rss-item {
background: rgba(255,255,255,0.1);
border-radius: 5px;
padding: 8px;
margin: 5px 0;
font-size: 0.8rem;
}
.error-message {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
padding: 8px;
border-radius: 5px;
margin: 5px 0;
}
.success-message {
color: #10b981;
background: rgba(16, 185, 129, 0.1);
padding: 8px;
border-radius: 5px;
margin: 5px 0;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.dark .glass-effect {
background: rgba(0,0,0,0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
}
.dark .timeline-post {
background: #374151;
color: #f9fafb;
}
@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>
<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>Verse
<span class="text-sm opacity-75 ml-2">v2.0 完全修正版</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">
<div class="font-semibold text-lg" id="header-username">未設定</div>
<div class="text-sm opacity-75">ユーザー</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>
</div>
</div>
</header>
<div class="max-w-6xl mx-auto px-4 py-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 左側カラム -->
<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>
<!-- 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>マルコフ連鎖では過去の投稿からランダムな文章を生成します
</div>
<div id="bot-log" class="log-container text-white text-xs"></div>
</div>
</div>
<!-- RSS機能 -->
<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 class="space-y-4">
<div>
<input type="text" id="feedUrl" class="w-full p-3 border rounded-lg bg-white bg-opacity-90" placeholder="RSS URL">
<button onclick="registerFeedUrl()" class="btn-primary w-full mt-2 py-2 rounded-lg text-white">
<i class="fas fa-plus mr-2"></i>フィード登録
</button>
</div>
<div>
<input type="number" id="feedIntervalSec" class="w-full p-2 border rounded-lg bg-white bg-opacity-90" placeholder="RSS自動取得間隔(秒)" min="60" max="3600" value="180">
<div class="flex space-x-2 mt-2">
<button onclick="fetchAllFeeds()" class="btn-primary flex-1 py-2 rounded-lg text-white">
<i class="fas fa-download mr-2"></i>手動取得
</button>
<button onclick="startRSSAutoPost()" class="btn-primary flex-1 py-2 rounded-lg text-white">
<i class="fas fa-play mr-2"></i>自動開始
</button>
<button onclick="stopRSSAutoPost()" 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="bg-white bg-opacity-80 rounded-lg p-3">
<div class="flex justify-between items-center mb-2">
<h5 class="font-semibold text-gray-800">登録済みフィード:</h5>
<button onclick="addDefaultFeeds()" class="text-xs bg-blue-500 text-white px-2 py-1 rounded">
デフォルト追加
</button>
</div>
<div id="feed-list" class="text-sm text-gray-700"></div>
</div>
<div id="rss-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-75">
<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-75">
<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-75">
<i class="fas fa-copyright mr-2"></i>2025 Verse - 次世代ソーシャルネットワーク v2.0
<span class="ml-4">
<i class="fas fa-bug mr-1"></i>RSS自動投稿完全修正版
</span>
</p>
</footer>
<script>
// === 設定とグローバル変数 ===
const DEFAULT_FEEDS = [
"https://feeds.feedburner.com/hatena/b/hotentry",
"https://gigazine.net/news/rss_2.0/",
"https://rss.cnn.com/rss/edition.rss",
"https://feeds.bbci.co.uk/news/rss.xml",
"https://www.reddit.com/r/worldnews/.rss"
];
const RSS_APIS = [
'https://api.rss2json.com/v1/api.json',
'https://cors-anywhere.herokuapp.com/',
'https://api.allorigins.win/get'
];
// グローバル状態
let posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
let feedUrls = JSON.parse(localStorage.getItem('verse_feeds') || '[]');
let profile = JSON.parse(localStorage.getItem('verse_profile') || '{}');
let processedItems = new Set(JSON.parse(localStorage.getItem('verse_processed') || '[]'));
// タイマーとステータス
let botInterval = null;
let rssInterval = null;
let isDarkMode = localStorage.getItem('verse_darkMode') === 'true';
let markovChain = null;
let isInitialized = false;
// 統計情報
let stats = {
totalPosts: 0,
rssSuccess: 0,
rssErrors: 0,
botPosts: 0,
lastRSSUpdate: null,
lastBotPost: null
};
// === 初期化処理 ===
function initializeApp() {
if (isInitialized) return;
// プロフィール初期化
if (!profile.icon) profile.icon = 'https://via.placeholder.com/100';
if (!profile.username) profile.username = 'ゲストユーザー';
if (!profile.selfIntro) profile.selfIntro = '';
// デフォルトフィード設定
if (feedUrls.length === 0) {
feedUrls = [...DEFAULT_FEEDS];
saveData();
addLog('rss-log', 'デフォルトRSSフィードを登録しました', 'success');
}
// UI初期化
updateAllUI();
updateStatusIndicators();
// ダークモード適用
if (isDarkMode) {
document.body.classList.add('dark');
document.body.style.background = 'linear-gradient(135deg, #1a202c 0%, #2d3748 100%)';
}
isInitialized = true;
addLog('rss-log', 'アプリケーション初期化完了', 'success');
addLog('bot-log', 'BOT機能初期化完了', 'success');
// 初回RSS取得(遅延実行)
setTimeout(() => {
addLog('rss-log', '初回RSS取得を開始します...', 'info');
fetchAllFeeds();
}, 3000);
}
// === データ管理 ===
function saveData() {
try {
localStorage.setItem('verse_posts', JSON.stringify(posts));
localStorage.setItem('verse_feeds', JSON.stringify(feedUrls));
localStorage.setItem('verse_profile', JSON.stringify(profile));
localStorage.setItem('verse_processed', JSON.stringify([...processedItems]));
localStorage.setItem('verse_darkMode', isDarkMode);
stats.totalPosts = posts.length;
} catch (error) {
console.error('データ保存エラー:', error);
addLog('rss-log', `データ保存エラー: ${error.message}`, 'error');
}
}
function clearAllPosts() {
if (confirm('すべての投稿を削除してもよろしいですか?\nこの操作は元に戻せません。')) {
posts = [];
processedItems.clear();
saveData();
renderTimeline();
addLog('rss-log', 'すべての投稿を削除しました', 'info');
addLog('bot-log', 'すべての投稿を削除しました', 'info');
}
}
function exportData() {
const exportData = {
posts: posts,
profile: profile,
feedUrls: feedUrls,
stats: stats,
exportDate: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `verse_export_${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
addLog('rss-log', 'データをエクスポートしました', 'success');
}
// === ログ機能 ===
function addLog(elementId, message, type = 'info') {
const logElement = document.getElementById(elementId);
if (!logElement) return;
const timestamp = new Date().toLocaleTimeString('ja-JP');
const logEntry = document.createElement('div');
const typeClass = {
'error': 'error-message',
'success': 'success-message',
'info': 'text-white opacity-75'
}[type] || 'text-white opacity-75';
logEntry.className = typeClass;
logEntry.innerHTML = `<span class="opacity-75">[${timestamp}]</span> ${message}`;
logElement.appendChild(logEntry);
logElement.scrollTop = logElement.scrollHeight;
// ログ数制限
while (logElement.children.length > 50) {
logElement.removeChild(logElement.firstChild);
}
}
// === ステータス管理 ===
function updateStatusIndicators() {
const botIndicator = document.getElementById('bot-status');
const rssIndicator = document.getElementById('rss-status');
const botStatusText = document.getElementById('bot-status-text');
const rssStatusText = document.getElementById('rss-status-text');
if (botIndicator && botStatusText) {
const isActive = botInterval !== null;
botIndicator.className = `status-indicator ${isActive ? 'status-active' : 'status-inactive'}`;
botStatusText.textContent = isActive ? '動作中' : '停止中';
}
if (rssIndicator && rssStatusText) {
const isActive = rssInterval !== null;
rssIndicator.className = `status-indicator ${isActive ? 'status-active' : 'status-inactive'}`;
rssStatusText.textContent = isActive ? '動作中' : '停止中';
}
}
function showSystemStatus() {
const statusInfo = `
=== Verse システムステータス ===
総投稿数: ${stats.totalPosts}
RSS成功: ${stats.rssSuccess}
RSSエラー: ${stats.rssErrors}
BOT投稿数: ${stats.botPosts}
最後のRSS更新: ${stats.lastRSSUpdate || 'なし'}
最後のBOT投稿: ${stats.lastBotPost || 'なし'}
RSS自動取得: ${rssInterval ? '動作中' : '停止中'}
BOT自動投稿: ${botInterval ? '動作中' : '停止中'}
登録フィード数: ${feedUrls.length}
処理済みアイテム: ${processedItems.size}
`;
alert(statusInfo);
}
// === 投稿管理 ===
function createPost(content, type = 'user', username = null, icon = null) {
if (!content || !content.trim()) {
addLog('rss-log', '空のコンテンツは投稿できません', 'error');
return false;
}
// RSS重複チェック
if (type === 'feed') {
const urlMatch = content.match(/href="([^"]+)"/);
const url = urlMatch ? urlMatch[1] : null;
if (url && processedItems.has(url)) {
addLog('rss-log', `重複記事をスキップ: ${url.substring(0, 50)}...`, 'info');
return false;
}
if (url) {
processedItems.add(url);
}
}
const post = {
id: Date.now() + Math.random(),
content: content.trim(),
likes: 0,
timestamp: new Date().toLocaleString('ja-JP'),
type: type,
username: username || profile.username,
icon: icon || profile.icon
};
posts.unshift(post);
// 統計更新
if (type === 'bot' || type === 'markov') {
stats.botPosts++;
stats.lastBotPost = post.timestamp;
}
if (type === 'feed') {
stats.rssSuccess++;
stats.lastRSSUpdate = post.timestamp;
}
saveData();
renderTimeline();
return true;
}
function createUserPost() {
const content = document.getElementById('postContent').value.trim();
if (content) {
if (createPost(content, 'user')) {
document.getElementById('postContent').value = '';
updateCharCount();
addLog('bot-log', 'ユーザー投稿を作成しました', 'success');
}
}
}
function likePost(id) {
const post = posts.find(p => p.id === id);
if (post) {
post.likes++;
saveData();
renderTimeline();
}
}
function deletePost(id) {
if (confirm('この投稿を削除しますか?')) {
const post = posts.find(p => p.id === id);
if (post && post.type === 'feed') {
const urlMatch = post.content.match(/href="([^"]+)"/);
if (urlMatch) {
processedItems.delete(urlMatch[1]);
}
}
posts = posts.filter(p => p.id !== id);
saveData();
renderTimeline();
}
}
// === タイムライン表示 ===
function renderTimeline() {
const timeline = document.getElementById('timeline');
const emptyTimeline = document.getElementById('empty-timeline');
const postCount = document.getElementById('post-count');
if (posts.length === 0) {
timeline.innerHTML = '';
emptyTimeline.style.display = 'block';
postCount.textContent = '(0件の投稿)';
return;
}
emptyTimeline.style.display = 'none';
postCount.textContent = `(${posts.length}件の投稿)`;
timeline.innerHTML = posts.map(post => {
const typeInfo = getPostTypeInfo(post.type);
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="${post.icon}" alt="アバター" class="w-10 h-10 rounded-full object-cover">
<div>
<div class="flex items-center">
<span class="font-semibold text-gray-800 dark:text-white">${post.username}</span>
<span class="username-badge">${typeInfo.badge}</span>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${post.timestamp}</div>
</div>
</div>
</div>
<div class="text-gray-800 dark:text-gray-200 mb-4 leading-relaxed">${post.content}</div>
<div class="flex items-center space-x-4 pt-4 border-t border-gray-100 dark:border-gray-600">
<button onclick="likePost(${post.id})" class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-red-500 transition-colors">
<i class="fas fa-heart"></i>
<span>${post.likes}</span>
</button>
<div class="relative">
<button onclick="toggleShareMenu(${post.id})" class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-blue-500 transition-colors">
<i class="fas fa-share"></i>
<span>シェア</span>
</button>
<div id="share-menu-${post.id}" class="share-menu hidden">
<button onclick="shareToX(${post.id})"><i class="fab fa-x-twitter text-blue-400 mr-2"></i>Xでシェア</button>
<button onclick="shareToLine(${post.id})"><i class="fab fa-line text-green-400 mr-2"></i>LINEでシェア</button>
<button onclick="copyPost(${post.id})"><i class="fas fa-copy mr-2"></i>コピー</button>
</div>
</div>
<button onclick="deletePost(${post.id})" class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-red-500 transition-colors ml-auto">
<i class="fas fa-trash"></i>
<span>削除</span>
</button>
</div>
</div>
`;
}).join('');
}
function getPostTypeInfo(type) {
const typeMap = {
'bot': { badge: '🤖 BOT' },
'markov': { badge: '🎲 MarkovBOT' },
'feed': { badge: '📡 RSS Feed' },
'user': { badge: '👤 ユーザー' }
};
return typeMap[type] || typeMap['user'];
}
// === シェア機能 ===
function getPostContentText(id) {
const post = posts.find(p => p.id === id);
if (!post) return '';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = post.content;
return tempDiv.textContent || tempDiv.innerText || '';
}
function toggleShareMenu(id) {
document.querySelectorAll('[id^="share-menu-"]').forEach(el => el.classList.add('hidden'));
const menu = document.getElementById('share-menu-' + id);
if (menu) {
menu.classList.toggle('hidden');
setTimeout(() => {
const closeMenu = (e) => {
if (!menu.contains(e.target)) {
menu.classList.add('hidden');
document.removeEventListener('mousedown', closeMenu);
}
};
document.addEventListener('mousedown', closeMenu);
}, 100);
}
}
function shareToX(id) {
const text = encodeURIComponent(getPostContentText(id));
const url = encodeURIComponent(location.href);
window.open(`https://twitter.com/intent/tweet?text=${text}&url=${url}`, '_blank');
}
function shareToLine(id) {
const text = encodeURIComponent(getPostContentText(id) + ' ' + location.href);
window.open(`https://social-plugins.line.me/lineit/share?url=${text}`, '_blank');
}
function copyPost(id) {
const text = getPostContentText(id);
navigator.clipboard.writeText(text).then(() => {
alert('投稿内容をクリップボードにコピーしました!');
}).catch(() => {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
alert('投稿内容をコピーしました!');
});
}
// === BOT機能 ===
function postBotMessage() {
const content = document.getElementById('botContent').value.trim();
if (content) {
if (createPost(content, 'bot', 'BOT')) {
document.getElementById('botContent').value = '';
addLog('bot-log', `BOT投稿: "${content.substring(0, 30)}..."`, 'success');
}
}
}
// === マルコフ連鎖機能(強化版) ===
function initializeMarkovChain() {
try {
if (typeof RiTa === 'undefined') {
addLog('bot-log', 'RiTaライブラリが未読み込みです', 'error');
return false;
}
markovChain = new RiTa.Markov(3);
const userPosts = posts.filter(p =>
(p.type === 'user' || p.type === 'bot') &&
p.content.length > 20 &&
!p.content.includes('<div') &&
!p.content.includes('href=')
);
if (userPosts.length < 5) {
addLog('bot-log', `学習データ不足 (${userPosts.length}件) - 5件以上必要`, 'error');
return false;
}
const textData = userPosts
.map(p => cleanTextForMarkov(p.content))
.filter(text => text.length > 15)
.join('\n');
if (textData.length < 200) {
addLog('bot-log', 'テキストデータが不十分です', 'error');
return false;
}
markovChain.addText(textData);
addLog('bot-log', `マルコフ連鎖学習完了 (${userPosts.length}件の投稿を学習)`, 'success');
return true;
} catch (error) {
addLog('bot-log', `マルコフ初期化エラー: ${error.message}`, 'error');
return false;
}
}
function cleanTextForMarkov(text) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = text;
let cleanText = tempDiv.textContent || tempDiv.innerText || '';
cleanText = cleanText
.replace(/https?:\/\/[^\s]+/g, '')
.replace(/[^\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\u3400-\u4DBFa-zA-Z0-9\s!?。、.,!?\n]/g, '')
.replace(/\s+/g, ' ')
.replace(/[。!?]{2,}/g, '。')
.trim();
return cleanText;
}
function generateMarkovText() {
try {
if (!initializeMarkovChain()) {
const fallbackMessages = [
"今日はいい天気ですね!どのように過ごされていますか?",
"最近面白いニュースありましたか?",
"新しいアイデアが浮かんできました。",
"皆さんはどう思いますか?",
"今日も一日頑張りましょう!"
];
return fallbackMessages[Math.floor(Math.random() * fallbackMessages.length)];
}
let bestText = '';
const maxAttempts = 15;
for (let i = 0; i < maxAttempts; i++) {
try {
const options = {
maxLength: 120,
minLength: 20,
count: Math.floor(Math.random() * 2) + 1
};
const sentences = markovChain.generate(options);
if (sentences && sentences.length > 0) {
let generatedText = sentences.join(' ').trim();
// 文章の自然な終端処理
if (generatedText.length > 150) {
const endMarkers = ['。', '!', '?'];
let bestEnd = -1;
endMarkers.forEach(marker => {
const pos = generatedText.lastIndexOf(marker, 130);
if (pos > 30) bestEnd = Math.max(bestEnd, pos);
});
if (bestEnd > -1) {
generatedText = generatedText.substring(0, bestEnd + 1);
}
}
// 品質チェック
if (generatedText.length >= 20 &&
generatedText.length <= 200 &&
!generatedText.includes('undefined') &&
generatedText.length > bestText.length) {
bestText = generatedText;
}
}
} catch (genError) {
continue;
}
}
if (!bestText || bestText.length < 10) {
bestText = "新しいアイデアについて考えています。皆さんの意見も聞きたいですね。";
}
return bestText;
} catch (error) {
addLog('bot-log', `マルコフ生成エラー: ${error.message}`, 'error');
return "マルコフ連鎖で自然な文章を生成しています。";
}
}
function postMarkovBot() {
const content = generateMarkovText();
if (createPost(content, 'markov', 'MarkovBOT')) {
addLog('bot-log', `マルコフ投稿: "${content.substring(0, 40)}..."`, 'success');
}
}
function startBotAutoPost() {
const interval = parseInt(document.getElementById('botIntervalSec').value) || 60;
if (interval < 10) {
alert('間隔は10秒以上で設定してください。');
return;
}
stopBotAutoPost();
// 初回投稿
setTimeout(postMarkovBot, 2000);
botInterval = setInterval(() => {
postMarkovBot();
}, interval * 1000);
updateStatusIndicators();
addLog('bot-log', `マルコフBOT自動投稿開始 (${interval}秒間隔)`, 'success');
}
function stopBotAutoPost() {
if (botInterval) {
clearInterval(botInterval);
botInterval = null;
updateStatusIndicators();
addLog('bot-log', 'マルコフBOT自動投稿を停止しました', 'info');
}
}
// === RSS機能(完全修正版) ===
function addDefaultFeeds() {
let addedCount = 0;
DEFAULT_FEEDS.forEach(url => {
if (!feedUrls.includes(url)) {
feedUrls.push(url);
addedCount++;
}
});
if (addedCount > 0) {
saveData();
renderFeedList();
addLog('rss-log', `${addedCount}件のデフォルトフィードを追加しました`, 'success');
} else {
addLog('rss-log', 'すべてのデフォルトフィードは既に登録されています', 'info');
}
}
function registerFeedUrl() {
const url = document.getElementById('feedUrl').value.trim();
if (!url) {
alert('URLを入力してください。');
return;
}
if (!url.startsWith('http')) {
alert('有効なURLを入力してください(http://またはhttps://で開始)。');
return;
}
if (feedUrls.includes(url)) {
alert('このフィードは既に登録されています。');
return;
}
feedUrls.push(url);
saveData();
renderFeedList();
document.getElementById('feedUrl').value = '';
addLog('rss-log', `フィード登録: ${url}`, 'success');
}
function removeFeedUrl(index) {
if (confirm('このフィードを削除しますか?')) {
const removedUrl = feedUrls[index];
feedUrls.splice(index, 1);
saveData();
renderFeedList();
addLog('rss-log', `フィード削除: ${removedUrl}`, 'info');
}
}
function renderFeedList() {
const feedList = document.getElementById('feed-list');
if (feedUrls.length === 0) {
feedList.innerHTML = '<div class="text-gray-500">登録されたフィードはありません</div>';
return;
}
feedList.innerHTML = feedUrls.map((url, index) => `
<div class="rss-item flex items-center justify-between">
<span class="text-xs truncate flex-1" title="${url}">${url}</span>
<button onclick="removeFeedUrl(${index})" class="text-red-500 hover:text-red-700 ml-2 text-xs">
<i class="fas fa-times"></i>
</button>
</div>
`).join('');
}
// 改良されたRSS取得機能
async function fetchAllFeeds() {
if (feedUrls.length === 0) {
addLog('rss-log', 'RSSフィードが登録されていません', 'error');
return;
}
addLog('rss-log', `RSS取得開始 (${feedUrls.length}件のフィード)`, 'info');
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < feedUrls.length; i++) {
const url = feedUrls[i];
try {
addLog('rss-log', `[${i + 1}/${feedUrls.length}] 取得中: ${url.substring(0, 50)}...`, 'info');
const success = await fetchSingleFeed(url);
if (success) {
successCount++;
addLog('rss-log', `✓ 成功: ${url.substring(0, 50)}...`, 'success');
} else {
errorCount++;
addLog('rss-log', `✗ 失敗: ${url.substring(0, 50)}...`, 'error');
}
} catch (error) {
errorCount++;
addLog('rss-log', `✗ エラー: ${error.message}`, 'error');
}
// レート制限対策(フィード間に待機時間)
if (i < feedUrls.length - 1) {
await sleep(2000);
}
}
stats.rssSuccess += successCount;
stats.rssErrors += errorCount;
const resultMsg = `RSS取得完了: 成功 ${successCount}件 / エラー ${errorCount}件`;
addLog('rss-log', resultMsg, successCount > 0 ? 'success' : 'error');
if (successCount > 0) {
saveData();
}
}
async function fetchSingleFeed(feedUrl) {
const apis = [
// API 1: rss2json
async () => {
const response = await fetchWithTimeout(
`https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent(feedUrl)}&count=5`,
10000
);
const data = await response.json();
if (data.status !== 'ok') {
throw new Error(data.message || 'RSS API error');
}
return data.items || [];
},
// API 2: allorigins
async () => {
const response = await fetchWithTimeout(
`https://api.allorigins.win/get?url=${encodeURIComponent(feedUrl)}`,
10000
);
const data = await response.json();
if (!data.contents) {
throw new Error('No contents returned');
}
return parseRSSContent(data.contents);
}
];
for (const api of apis) {
try {
const items = await api();
return await processRSSItems(items, feedUrl);
} catch (error) {
continue;
}
}
return false;
}
async function processRSSItems(items, feedUrl) {
if (!items || items.length === 0) {
return false;
}
let processedCount = 0;
for (const item of items.slice(0, 3)) { // 最大3件まで
if (!item.title || !item.link) continue;
// 重複チェック
if (processedItems.has(item.link)) {
continue;
}
const cleanTitle = cleanHTML(item.title).substring(0, 100);
const cleanDescription = item.description
? cleanHTML(item.description).substring(0, 150)
: '';
const content = createRSSPostContent(cleanTitle, cleanDescription, item.link, feedUrl);
if (createPost(content, 'feed', 'RSS Feed')) {
processedCount++;
processedItems.add(item.link);
// 投稿間の間隔
if (processedCount < 3) {
await sleep(1000);
}
}
}
return processedCount > 0;
}
function createRSSPostContent(title, description, link, feedUrl) {
const feedDomain = new URL(feedUrl).hostname;
return `
<div class="border-l-4 border-blue-500 pl-4 bg-gray-50 dark:bg-gray-700 rounded-r-lg p-3">
<h4 class="font-semibold mb-2 text-gray-900 dark:text-white">${title}</h4>
${description ? `<p class="text-sm text-gray-600 dark:text-gray-300 mb-3">${description}${description.length >= 150 ? '...' : ''}</p>` : ''}
<div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">
<i class="fas fa-globe mr-1"></i>${feedDomain}
</span>
<a href="${link}" target="_blank" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 text-sm font-medium">
<i class="fas fa-external-link-alt mr-1"></i>記事を読む
</a>
</div>
</div>
`;
}
function parseRSSContent(xmlContent) {
// 簡易XMLパース(実装を簡略化)
const items = [];
const parser = new DOMParser();
const doc = parser.parseFromString(xmlContent, 'text/xml');
const rssItems = doc.querySelectorAll('item');
rssItems.forEach(item => {
const title = item.querySelector('title')?.textContent;
const link = item.querySelector('link')?.textContent;
const description = item.querySelector('description')?.textContent;
if (title && link) {
items.push({ title, link, description });
}
});
return items;
}
function cleanHTML(text) {
if (!text) return '';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = text;
return (tempDiv.textContent || tempDiv.innerText || '').trim();
}
async function fetchWithTimeout(url, timeout = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal,
headers: {
'Accept': 'application/json',
'User-Agent': 'Verse RSS Reader 2.0'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
} finally {
clearTimeout(timeoutId);
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function startRSSAutoPost() {
const interval = parseInt(document.getElementById('feedIntervalSec').value) || 180;
if (interval < 60) {
alert('間隔は60秒以上で設定してください。');
return;
}
stopRSSAutoPost();
// 初回実行(少し遅延)
setTimeout(() => {
addLog('rss-log', '自動RSS取得の初回実行を開始...', 'info');
fetchAllFeeds();
}, 5000);
rssInterval = setInterval(() => {
addLog('rss-log', '定期RSS取得を実行中...', 'info');
fetchAllFeeds();
}, interval * 1000);
updateStatusIndicators();
addLog('rss-log', `RSS自動取得開始 (${interval}秒間隔)`, 'success');
}
function stopRSSAutoPost() {
if (rssInterval) {
clearInterval(rssInterval);
rssInterval = null;
updateStatusIndicators();
addLog('rss-log', 'RSS自動取得を停止しました', 'info');
}
}
// === プロフィール機能 ===
function uploadProfileIcon(event) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
alert('画像サイズは5MB以下にしてください。');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
profile.icon = e.target.result;
updateAllUI();
saveData();
alert('プロフィール画像を更新しました!');
};
reader.readAsDataURL(file);
}
function saveProfile() {
const username = document.getElementById('username').value.trim();
const selfIntro = document.getElementById('self-intro').value.trim();
if (username && username.length > 20) {
alert('ユーザー名は20文字以内で入力してください。');
return;
}
profile.username = username || 'ゲストユーザー';
profile.selfIntro = selfIntro;
updateAllUI();
saveData();
alert('プロフィールを保存しました!');
}
// === UI更新 ===
function updateAllUI() {
// プロフィール画像更新
const profileIcon = document.getElementById('profile-icon');
const headerIcon = document.getElementById('header-profile-icon');
if (profileIcon) profileIcon.src = profile.icon;
if (headerIcon) headerIcon.src = profile.icon;
// ユーザー名更新
const usernameElements = ['username-preview', 'header-username'];
usernameElements.forEach(id => {
const element = document.getElementById(id);
if (element) element.textContent = profile.username;
});
// 自己紹介更新
const selfIntroPreview = document.getElementById('self-intro-preview');
if (selfIntroPreview) {
selfIntroPreview.textContent = profile.selfIntro || 'まだ自己紹介がありません';
}
// タイムライン再描画
renderTimeline();
renderFeedList();
}
function updateCharCount() {
const postContent = document.getElementById('postContent');
const charCount = document.getElementById('char-count');
if (postContent && charCount) {
const length = postContent.value.length;
charCount.textContent = `(${length}/500)`;
charCount.style.color = length > 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%)';
}
saveData();
}
// === イベントリスナー ===
document.addEventListener('DOMContentLoaded', function() {
// 文字数カウンター
const postContent = document.getElementById('postContent');
if (postContent) {
postContent.addEventListener('input', updateCharCount);
}
// プロフィール入力のリアルタイム更新
const usernameInput = document.getElementById('username');
const selfIntroInput = document.getElementById('self-intro');
if (usernameInput) {
usernameInput.addEventListener('input', function() {
const username = this.value.trim() || 'ゲストユーザー';
document.getElementById('username-preview').textContent = username;
document.getElementById('header-username').textContent = username;
});
}
if (selfIntroInput) {
selfIntroInput.addEventListener('input', function() {
const selfIntro = this.value.trim() || 'まだ自己紹介がありません';
document.getElementById('self-intro-preview').textContent = selfIntro;
});
}
});
// === 初期化実行 ===
window.addEventListener('load', function() {
// アプリ初期化
initializeApp();
// プロフィール情報を入力フィールドに設定
document.getElementById('username').value = profile.username || '';
document.getElementById('self-intro').value = profile.selfIntro || '';
// ページ終了時のクリーンアップ
window.addEventListener('beforeunload', function() {
stopBotAutoPost();
stopRSSAutoPost();
saveData();
});
});
</script>
</body>
</html>
カテゴリー: programming
VerseVR
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Verse – プロダクション級ソーシャルVR</title>
<!-- A-Frame Core -->
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
<!-- Networked-AFrame for multi-user sync -->
<script src="https://unpkg.com/networked-aframe@0.8.0/dist/networked-aframe.min.js"></script>
<!-- GUI for VR buttons -->
<script src="https://unpkg.com/aframe-gui/dist/aframe-gui.min.js"></script>
<!-- Environment presets -->
<script src="https://unpkg.com/aframe-environment-component/dist/aframe-environment-component.min.js"></script>
<!-- Extras: teleport, locomotion, physics, super-hands -->
<script src="https://unpkg.com/aframe-extras@6.1.1/dist/aframe-extras.min.js"></script>
<script src="https://unpkg.com/aframe-super-hands-component@4.0.3/dist/aframe-super-hands.min.js"></script>
<!-- Socket.IO for signaling -->
<script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
<!-- Simple-Peer for WebRTC voice chat -->
<script src="https://unpkg.com/simple-peer@9.11.0/simplepeer.min.js"></script>
<style>
body { margin: 0; }
#vr-scene { width: 100%; height: 100vh; }
.interactive:hover { animation: pulse 0.5s infinite alternate; }
@keyframes pulse { to { scale: 1.05; } }
</style>
</head>
<body>
<a-scene id="vr-scene"
networked-scene="room: verse-room; serverURL: https://YOUR_SIGNAL_SERVER; app: verse-vr; debug: false"
environment="preset: forest; groundColor:#445; skyColor:#889"
extras="teleportControls: true; locomotionControls: true"
physics="gravity: -9.8; debug: false">
<!-- Assets -->
<a-assets>
<audio id="click-sound" src="click.mp3"></audio>
<a-asset-item id="avatarModel" src="avatar.glb"></a-asset-item>
<img id="panel-bg" src="panel-bg.png" />
</a-assets>
<!-- Camera Rig -->
<a-entity id="cameraRig" position="0 1.6 0"
locomotion-controls="fly:false; speed:4"
teleport-controls="button: trigger; collisionEntities: #ground">
<a-entity camera look-controls>
<a-cursor fuse="true" fuseTimeout="300" material="color:cyan; shader:flat"></a-cursor>
</a-entity>
<a-entity hand-tracking-controls="hand: left" super-hands></a-entity>
<a-entity hand-tracking-controls="hand: right" super-hands></a-entity>
</a-entity>
<!-- Ground -->
<a-plane id="ground" position="0 0 0" rotation="-90 0 0" width="50" height="50" color="#444" static-body></a-plane>
<!-- Avatar Sync Template -->
<template id="avatar-template">
<a-entity>
<a-gltf-model src="#avatarModel" scale="0.6 0.6 0.6"></a-gltf-model>
</a-entity>
</template>
<a-entity networked-avatar networked="template:#avatar-template; attachTemplateToLocal:false"></a-entity>
<!-- GUI Menu -->
<a-gui-flex-container id="menu" flex-direction="row" justify-content="space-around"
panel-width="2" panel-height="0.2"
component-padding="0.05 0.1"
position="0 2 -1.5" material="src:#panel-bg; transparent:true">
<a-gui-button id="btnTextPost" value="Text Post" on-click="openTextInput()" font-size="28px" color="black"></a-gui-button>
<a-gui-button id="btnVoicePost" value="Voice Post" on-click="startVoiceRecognition()" font-size="28px" color="black"></a-gui-button>
<a-gui-button id="btnGPTChat" value="GPT BOT" on-click="callGPTBot()" font-size="28px" color="black"></a-gui-button>
<a-gui-button id="btnLikeMode" value="Like/Delete" on-click="toggleLikeMode()" font-size="28px" color="black"></a-gui-button>
<a-gui-button id="btnSpawnCube" value="Spawn Cube" on-click="spawnCube()" font-size="28px" color="black"></a-gui-button>
<a-gui-button id="btnVoiceChat" value="Voice Chat" on-click="toggleVoiceChat()" font-size="28px" color="black"></a-gui-button>
</a-gui-flex-container>
<!-- Containers -->
<a-entity id="post-container"></a-entity>
<a-entity id="ugc-container"></a-entity>
</a-scene>
<script>
// ----------- データ -----------
let posts = JSON.parse(localStorage.getItem('posts')||'[]');
let likeMode = false;
let recognition;
let socket = io('https://YOUR_SIGNAL_SERVER');
let peers = {};
let localStream;
// ----------- 投稿レンダリング -----------
function renderPosts() {
const container = document.getElementById('post-container');
container.innerHTML = '';
posts.forEach((p,i)=>{
const angle = (i/posts.length)*Math.PI*2;
const x = Math.cos(angle)*2;
const z = Math.sin(angle)*2;
const postEl = document.createElement('a-entity');
postEl.classList.add('interactive');
postEl.setAttribute('geometry','primitive: plane; width:1.2; height:0.5');
postEl.setAttribute('material','color:#fff; shader:flat');
postEl.setAttribute('position',`${x} 1.3 ${z}`);
postEl.setAttribute('rotation',`0 ${-angle*180/Math.PI+90} 0`);
postEl.setAttribute('text',`value:${p.content}; width:1.1; align:center; color:#000`);
// クリック処理
postEl.addEventListener('click', ()=>{
if(likeMode) {
p.likes = (p.likes||0)+1;
savePosts(); renderPosts();
}
});
// 削除ボタン
if(likeMode) {
const delBtn = document.createElement('a-entity');
delBtn.setAttribute('geometry','primitive: plane; width:0.2; height:0.1');
delBtn.setAttribute('material','color:#f88; shader:flat');
delBtn.setAttribute('position','0.55 -0.25 0.01');
delBtn.setAttribute('text','value:Del; width:0.2; align:center; color:#000');
delBtn.addEventListener('click', ()=>{ posts.splice(i,1); savePosts(); renderPosts(); });
postEl.appendChild(delBtn);
}
container.appendChild(postEl);
});
}
function savePosts(){ localStorage.setItem('posts', JSON.stringify(posts)); }
// ----------- テキスト投稿 -----------
window.openTextInput = ()=>{
const txt = prompt('投稿内容を入力してください');
if(txt){ posts.unshift({content:txt, likes:0}); savePosts(); renderPosts(); socket.emit('new-post', txt); }
};
// ----------- 音声投稿 -----------
window.startVoiceRecognition = ()=>{
if(!('webkitSpeechRecognition' in window)) return alert('音声認識非対応');
recognition = new webkitSpeechRecognition();
recognition.lang='ja-JP'; recognition.interimResults=false;
recognition.onresult=e=>{ const txt=e.results[0][0].transcript; posts.unshift({content:txt,likes:0}); savePosts(); renderPosts(); };
recognition.onerror=()=>alert('認識エラー'); recognition.start();
};
// ----------- GPT BOT -----------
window.callGPTBot = async ()=>{
const key='YOUR_OPENAI_API_KEY';
const res=await fetch('https://api.openai.com/v1/chat/completions',{ method:'POST',
headers:{'Content-Type':'application/json','Authorization':`Bearer ${key}`},
body:JSON.stringify({ model:'gpt-4o-mini', messages:[{role:'system',content:'あなたはVR BOTです。'}]
.concat(posts.slice(0,5).map(p=>({role:'user',content:p.content}))), max_tokens:50 })
});
const js=await res.json();
const txt=js.choices[0].message.content.trim();
posts.unshift({content:`🤖 GPT: ${txt}`,likes:0}); savePosts(); renderPosts();
};
// ----------- いいね/削除モード -----------
window.toggleLikeMode = ()=>{ likeMode=!likeMode; renderPosts(); };
// ----------- UGC: キューブ生成 -----------
window.spawnCube = ()=>{
const c=document.createElement('a-box');
c.setAttribute('class','interactive');
c.setAttribute('position','0 1 -1'); c.setAttribute('depth','0.5'); c.setAttribute('height','0.5'); c.setAttribute('width','0.5');
c.setAttribute('material','color:#4CC3D9'); c.setAttribute('dynamic-body','');
c.setAttribute('grabbable',''); c.setAttribute('stretchable','');
document.getElementById('ugc-container').appendChild(c);
};
// ----------- Voice Chat -----------
window.toggleVoiceChat = ()=>{ localStream?stopVoiceChat():startVoiceChat(); };
async function startVoiceChat() {
localStream = await navigator.mediaDevices.getUserMedia({audio:true});
socket.emit('join-voice');
socket.on('signal', ({ id, signal })=>{
if(!peers[id]) createPeer(id, false);
peers[id].signal(signal);
});
socket.on('user-joined', id=>{ createPeer(id, true); });
function createPeer(id, initiator) {
const peer = new SimplePeer({ initiator, trickle:false, stream:localStream });
peer.on('signal', signal=>{ socket.emit('signal',{ id: socket.id, to:id, signal }); });
peer.on('stream', stream=>{ const e=document.createElement('audio'); e.srcObject=stream; e.autoplay=true; document.body.appendChild(e); });
peers[id]=peer;
}
}
function stopVoiceChat(){ Object.values(peers).forEach(p=>p.destroy()); peers={}; localStream.getTracks().forEach(t=>t.stop()); localStream=null; }
// ----------- Socket.IO イベント -----------
socket.on('new-post', txt=>{ posts.unshift({content:txt,likes:0}); savePosts(); renderPosts(); });
// ----------- 初期ロード -----------
document.querySelector('a-scene').addEventListener('loaded', ()=>{ renderPosts(); });
</script>
</body>
</html>
C++の基礎と応用
C++の基礎
ChatGPT:
C++の基礎を簡潔にまとめます。
■ 1. 基本構造
cppコピーする編集する#include <iostream> // 標準入出力
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
#include <iostream>: 入出力用int main(): プログラムの開始点std::cout: コンソール出力return 0: 正常終了
■ 2. 変数
cppコピーする編集するint number = 10;
double pi = 3.14;
char letter = 'A';
std::string text = "Hello";
bool flag = true;
■ 3. 条件分岐
cppコピーする編集するif (number > 5) {
std::cout << "Greater than 5" << std::endl;
} else {
std::cout << "5 or less" << std::endl;
}
■ 4. ループ
- for
cppコピーする編集するfor (int i = 0; i < 5; i++) {
std::cout << i << std::endl;
}
- while
cppコピーする編集するint i = 0;
while (i < 5) {
std::cout << i << std::endl;
i++;
}
■ 5. 関数
cppコピーする編集するint add(int a, int b) {
return a + b;
}
int main() {
std::cout << add(3, 4) << std::endl; // 7
return 0;
}
■ 6. クラス
cppコピーする編集するclass Person {
public:
std::string name;
void greet() {
std::cout << "Hello, " << name << std::endl;
}
};
int main() {
Person p;
p.name = "Taro";
p.greet();
return 0;
}
■ 7. ポインタ
cppコピーする編集するint num = 10;
int* ptr = #
std::cout << *ptr << std::endl; // 10
もし「もっと詳しく」「演習問題」など希望があれば教えてください。
あなた:
C++の応用
ChatGPT:
C++の応用的な内容を段階的に紹介します。
■ 1. 参照とポインタの応用
ポインタ
cppコピーする編集するvoid swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
参照
cppコピーする編集するvoid swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
■ 2. クラスのコンストラクタ・デストラクタ
cppコピーする編集するclass Person {
public:
std::string name;
Person(std::string n) : name(n) {
std::cout << name << " created" << std::endl;
}
~Person() {
std::cout << name << " destroyed" << std::endl;
}
};
■ 3. 継承とポリモーフィズム
cppコピーする編集するclass Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
void makeSound(Animal* a) {
a->speak();
}
■ 4. テンプレート
cppコピーする編集するtemplate <typename T>
T add(T a, T b) {
return a + b;
}
■ 5. 標準ライブラリ(STL)
ベクター
cppコピーする編集する#include <vector>
std::vector<int> numbers = {1, 2, 3, 4};
numbers.push_back(5);
マップ
cppコピーする編集する#include <map>
std::map<std::string, int> ages;
ages["Taro"] = 20;
■ 6. ラムダ式
cppコピーする編集するauto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4) << std::endl;
■ 7. スマートポインタ
cppコピーする編集する#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::shared_ptr<int> p2 = std::make_shared<int>(20);
これらは実践的なC++開発で頻出するテクニックです。
もっと深い例(デザインパターン、マルチスレッド、ファイルIOなど)も必要なら教えてください。
あなた:
もっと
ChatGPT:
さらに高度なC++の応用技術を紹介します。
実践プログラムやパフォーマンス向上に不可欠な技法です。
■ 8. デザインパターンの例:シングルトン
cppコピーする編集するclass Singleton {
private:
static Singleton* instance;
Singleton() {}
public:
static Singleton* getInstance() {
if (!instance)
instance = new Singleton();
return instance;
}
};
Singleton* Singleton::
匿名相談・共感プラットフォーム
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>匿名相談・共感プラットフォーム</title>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #eef1f5; margin: 0; padding: 20px; }
header { text-align: center; padding: 20px; background: #4a90e2; color: white; border-radius: 8px; }
.container { max-width: 800px; margin: 20px auto; }
.card { background: white; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 15px; margin-bottom: 20px; }
textarea, input[type="text"] { width: 100%; padding: 10px; margin: 5px 0; border: 1px solid #ccc; border-radius: 5px; }
button { background: #4a90e2; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; margin-right: 5px; }
button:hover { background: #357ab8; }
.comment { background: #f5f5f5; border-left: 3px solid #4a90e2; padding: 8px; margin-top: 8px; border-radius: 5px; }
.meta { font-size: 0.85em; color: #555; }
.tag { display: inline-block; background: #ddd; border-radius: 5px; padding: 2px 8px; margin-right: 5px; font-size: 0.8em; }
footer { text-align: center; font-size: 0.8em; color: #888; margin-top: 50px; }
.search-bar { margin-bottom: 20px; }
.delete-btn { background: #e94e4e; }
.delete-btn:hover { background: #b73939; }
.edit-btn { background: #f0ad4e; }
.edit-btn:hover { background: #d98c00; }
.copy-btn { background: #6cc070; }
.copy-btn:hover { background: #4e9f50; }
</style>
</head>
<body>
<header>
<h1>匿名相談・共感プラットフォーム</h1>
<p>悩みや相談を匿名で投稿し、共感やコメントをもらおう</p>
</header>
<div class="container">
<div class="card">
<h3>新しい相談を投稿</h3>
<input type="text" id="postTags" placeholder="タグ(カンマ区切り)">
<textarea id="postText" placeholder="あなたの悩みや相談を書いてください..."></textarea>
<button onclick="addPost()">投稿する</button>
</div>
<div class="card search-bar">
<input type="text" id="searchInput" placeholder="投稿検索..." oninput="searchPosts()">
</div>
<div id="postsContainer"></div>
</div>
<footer>
© 2025 匿名相談・共感プラットフォーム
</footer>
<script>
let posts = JSON.parse(localStorage.getItem('posts')) || [];
function addPost() {
const text = document.getElementById('postText').value.trim();
const tags = document.getElementById('postTags').value.split(',').map(tag => tag.trim()).filter(tag => tag);
if (text) {
posts.unshift({ text, tags, empathy: 0, comments: [], date: new Date().toLocaleString() });
document.getElementById('postText').value = '';
document.getElementById('postTags').value = '';
saveAndRender();
}
}
function addEmpathy(index) {
posts[index].empathy++;
saveAndRender();
}
function addComment(index, commentId) {
const input = document.getElementById(commentId);
const commentText = input.value.trim();
if (commentText) {
posts[index].comments.push({ text: commentText, date: new Date().toLocaleString() });
input.value = '';
saveAndRender();
}
}
function editPost(index) {
const newText = prompt('投稿内容を編集してください:', posts[index].text);
if (newText !== null) {
posts[index].text = newText;
saveAndRender();
}
}
function deletePost(index) {
if (confirm('この投稿を削除しますか?')) {
posts.splice(index, 1);
saveAndRender();
}
}
function copyPost(index) {
navigator.clipboard.writeText(posts[index].text)
.then(() => alert('投稿内容をコピーしました'))
.catch(() => alert('コピーに失敗しました'));
}
function saveAndRender() {
localStorage.setItem('posts', JSON.stringify(posts));
renderPosts();
}
function renderPosts(filteredPosts = posts) {
const container = document.getElementById('postsContainer');
container.innerHTML = '';
filteredPosts.forEach((post, index) => {
let postHtml = `
<div class="card">
<p>${post.text}</p>
<div>${post.tags.map(tag => `<span class='tag'>#${tag}</span>`).join(' ')}</div>
<p class="meta">投稿日: ${post.date}</p>
<button onclick="addEmpathy(${index})">共感 (${post.empathy})</button>
<button class="copy-btn" onclick="copyPost(${index})">コピー</button>
<button class="edit-btn" onclick="editPost(${index})">編集</button>
<button class="delete-btn" onclick="deletePost(${index})">削除</button>
<h4>コメント</h4>
<input type="text" id="commentInput${index}" placeholder="コメントを書く...">
<button onclick="addComment(${index}, 'commentInput${index}')">送信</button>
`;
post.comments.forEach(comment => {
postHtml += `<div class="comment">${comment.text}<div class="meta">${comment.date}</div></div>`;
});
postHtml += '</div>';
container.innerHTML += postHtml;
});
}
function searchPosts() {
const query = document.getElementById('searchInput').value.toLowerCase();
const filtered = posts.filter(post =>
post.text.toLowerCase().includes(query) ||
post.tags.some(tag => tag.toLowerCase().includes(query))
);
renderPosts(filtered);
}
renderPosts();
</script>
</body>
</html>
AIキャラクター掲示板
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AIキャラクター掲示板</title>
<style>
body {
font-family: Arial, sans-serif;
background: #e0e0e0;
margin: 0;
padding: 0;
}
header {
background: #3949ab;
color: white;
text-align: center;
padding: 15px;
}
.container {
max-width: 800px;
margin: 20px auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.post {
border-bottom: 1px solid #ddd;
padding: 10px 0;
display: flex;
align-items: flex-start;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
}
.post-content {
flex-grow: 1;
}
.character {
font-weight: bold;
color: #3949ab;
}
.timestamp {
font-size: 0.8em;
color: gray;
}
.message {
margin: 5px 0;
}
.likes {
font-size: 0.9em;
cursor: pointer;
color: #ff5722;
}
.input-group {
margin-top: 20px;
}
input, textarea {
width: 100%;
padding: 8px;
margin-top: 5px;
border-radius: 4px;
border: 1px solid #ccc;
box-sizing: border-box;
}
button {
margin-top: 10px;
padding: 10px 15px;
background: #3949ab;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background: #303f9f;
}
</style>
</head>
<body>
<header>
<h1>AIキャラクター掲示板</h1>
<p>投稿数: <span id="postCount">0</span></p>
</header>
<div class="container" id="board"></div>
<div class="container input-group">
<label>あなたの名前:</label>
<input type="text" id="username" placeholder="例: カナタ">
<label>メッセージ:</label>
<textarea id="userMessage" placeholder="メッセージを入力..."></textarea>
<button onclick="postMessage()">投稿</button>
</div>
<script>
let postCounter = 0;
const aiCharacters = [
{ name: 'ネオン', avatar: 'https://via.placeholder.com/40/673ab7/ffffff?text=N', replies: ['それ面白いね!', '詳しく教えて!', 'すごい!'] },
{ name: 'ルナ', avatar: 'https://via.placeholder.com/40/ff4081/ffffff?text=L', replies: ['なるほど!', '私もそう思うわ。', '参考になったわ。'] },
{ name: 'ソラ', avatar: 'https://via.placeholder.com/40/03a9f4/ffffff?text=S', replies: ['良い話だね!', '素敵だ!', 'もっと教えて!'] }
];
function postMessage() {
const username = document.getElementById('username').value || '名無しさん';
const message = document.getElementById('userMessage').value.trim();
if (!message) return;
addPost(username, message, 'https://via.placeholder.com/40/9e9e9e/ffffff?text=U');
document.getElementById('userMessage').value = '';
aiCharacters.forEach((char, index) => {
setTimeout(() => aiReply(char), 1000 * (index + 1));
});
}
function addPost(user, message, avatarUrl) {
postCounter++;
document.getElementById('postCount').textContent = postCounter;
const board = document.getElementById('board');
const time = new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
const post = document.createElement('div');
post.className = 'post';
post.innerHTML = `
<img src="${avatarUrl}" alt="avatar" class="avatar">
<div class="post-content">
<span class="character">${user}</span> <span class="timestamp">[${time}]</span>
<p class="message">${message}</p>
<span class="likes" onclick="likePost(this)">♡ いいね</span>
</div>`;
board.appendChild(post);
}
function aiReply(character) {
const reply = character.replies[Math.floor(Math.random() * character.replies.length)];
addPost(character.name, reply, character.avatar);
}
function likePost(element) {
let current = element.textContent;
if (current.includes('♡')) {
element.textContent = current.replace('♡', '❤️');
}
}
</script>
</body>
</html>
ギルド掲示板
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>仮想ギルド掲示板「ドラゴンズクレスト」</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
:root {
--bg: #1c1c2b;
--panel: #2e2e4d;
--card: #3a3a5f;
--accent: #6c63ff;
--text: #fff;
}
body {
margin: 0;
font-family: "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
}
header {
background: var(--panel);
padding: 20px;
text-align: center;
font-size: 1.8em;
}
nav {
display: flex;
justify-content: center;
gap: 20px;
background: var(--card);
padding: 10px;
}
nav button {
background: transparent;
border: none;
color: #ddd;
font-size: 1em;
padding: 10px 20px;
cursor: pointer;
}
nav button.active {
border-bottom: 2px solid var(--accent);
color: var(--accent);
}
.container {
display: grid;
grid-template-columns: 1fr 250px;
gap: 20px;
max-width: 1200px;
margin: 20px auto;
padding: 0 10px;
}
.main {
background: var(--card);
padding: 20px;
border-radius: 12px;
}
.sidebar {
background: var(--card);
padding: 15px;
border-radius: 12px;
}
.post-form textarea {
width: 100%;
height: 80px;
border-radius: 6px;
padding: 10px;
border: none;
resize: vertical;
}
.post-form button {
margin-top: 10px;
padding: 10px 20px;
background: var(--accent);
border: none;
border-radius: 6px;
color: #fff;
cursor: pointer;
}
.post {
background: #404070;
border-radius: 10px;
padding: 15px;
margin-top: 15px;
display: flex;
gap: 10px;
flex-direction: row;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #6c63ff;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.post-content {
flex: 1;
}
.meta {
font-size: 0.9em;
color: #aaa;
margin-bottom: 5px;
}
.actions button {
margin-right: 8px;
background: #5a5a8f;
border: none;
color: white;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
}
.reply-area {
margin-top: 10px;
}
.reply-area textarea {
width: 100%;
height: 50px;
margin-top: 5px;
padding: 6px;
border-radius: 4px;
border: none;
}
.reply {
margin-top: 5px;
font-size: 0.85em;
color: #ddd;
}
.guild-info, .members, .guild-schedule, .guild-rules {
margin-bottom: 20px;
}
.members ul {
list-style: none;
padding: 0;
}
.members li {
padding: 5px 0;
}
.members li.online::before {
content: "●";
color: #7fff7f;
margin-right: 5px;
}
.members li.offline::before {
content: "●";
color: #888;
margin-right: 5px;
}
@media (max-width: 800px) {
.container {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<header>仮想ギルド掲示板「ドラゴンズクレスト」</header>
<nav>
<button class="tab-button active" onclick="switchTab('general')">🏰 雑談</button>
<button class="tab-button" onclick="switchTab('raid')">⚔️ レイド</button>
<button class="tab-button" onclick="switchTab('trade')">💰 取引</button>
</nav>
<div class="container">
<div class="main">
<div class="post-form">
<textarea id="postText" placeholder="ギルドのみんなに伝えたいことは?"></textarea>
<button onclick="postMessage()">投稿する</button>
</div>
<div id="general" class="tab-content">
<!-- 投稿はここに追加されます -->
</div>
<div id="raid" class="tab-content" style="display:none;"></div>
<div id="trade" class="tab-content" style="display:none;"></div>
</div>
<div class="sidebar">
<div class="guild-info">
<h3>ギルド情報</h3>
<p>設立:2025年<br>メンバー数:32名<br>ギルマス:Reina</p>
</div>
<div class="guild-schedule">
<h3>今週のスケジュール</h3>
<ul>
<li>🗓️ 7月12日(土) 21:00〜:レイド「闇の塔」</li>
<li>🗓️ 7月14日(月) 22:00〜:PvP練習会</li>
<li>🗓️ 7月16日(水) 20:00〜:定例会議</li>
</ul>
</div>
<div class="guild-rules">
<h3>ギルドルール</h3>
<ol>
<li>他プレイヤーへの迷惑行為禁止</li>
<li>無断脱退・長期不在時は一言ください</li>
<li>レイド参加は20分前集合!</li>
</ol>
</div>
<div class="members">
<h3>メンバー(抜粋)</h3>
<ul>
<li class="online">Reina</li>
<li class="online">Shiro</li>
<li class="offline">Yuna</li>
<li class="online">Kuro</li>
<li class="offline">Mika</li>
</ul>
</div>
</div>
</div>
<script>
function postMessage() {
const textarea = document.getElementById("postText");
const content = textarea.value.trim();
if (!content) return;
const now = new Date().toLocaleString("ja-JP", { hour12: false });
const post = document.createElement("div");
post.className = "post";
post.innerHTML = `
<div class="avatar">Y</div>
<div class="post-content">
<div class="meta">あなた | ${now}</div>
<div class="content">${content.replace(/\n/g, "<br>")}</div>
<div class="actions">
<button onclick="likePost(this)">👍 <span>0</span></button>
<button onclick="toggleReply(this)">💬 返信</button>
<button onclick="deletePost(this)">🗑 削除</button>
</div>
<div class="reply-area" style="display:none;">
<textarea placeholder="返信を書く..."></textarea>
<button onclick="submitReply(this)">返信する</button>
</div>
<div class="replies"></div>
</div>
`;
const activeTab = document.querySelector(".tab-button.active").textContent.trim();
const tabId = activeTab.includes("雑談") ? "general" : activeTab.includes("レイド") ? "raid" : "trade";
document.getElementById(tabId).prepend(post);
textarea.value = "";
}
function switchTab(tabId) {
document.querySelectorAll(".tab-content").forEach(tab => tab.style.display = "none");
document.querySelectorAll(".tab-button").forEach(btn => btn.classList.remove("active"));
document.getElementById(tabId).style.display = "block";
event.target.classList.add("active");
}
function likePost(button) {
const span = button.querySelector("span");
span.textContent = parseInt(span.textContent) + 1;
}
function toggleReply(button) {
const replyArea = button.parentElement.nextElementSibling;
replyArea.style.display = replyArea.style.display === "none" ? "block" : "none";
}
function submitReply(button) {
const textarea = button.previousElementSibling;
const replyText = textarea.value.trim();
if (!replyText) return;
const replyDiv = document.createElement("div");
replyDiv.className = "reply";
replyDiv.innerHTML = `<small>あなた: ${replyText}</small>`;
button.closest(".post-content").querySelector(".replies").appendChild(replyDiv);
textarea.value = "";
}
function deletePost(button) {
if (confirm("この投稿を削除しますか?")) {
button.closest(".post").remove();
}
}
</script>
</body>
</html>
にじいろモール(beta) ECサイト
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>にじいろモール | オンラインショッピング</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="にじいろモールは、あらゆるジャンルの商品を取り揃えた総合ECサイトです。">
<link rel="icon" href="https://cdn-icons-png.flaticon.com/512/1170/1170576.png" type="image/png">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap" rel="stylesheet">
<style>
body {
margin: 0;
font-family: 'Noto Sans JP', sans-serif;
background: #f3f3f3;
}
header {
background-color: #5a4fcf;
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
header h1 {
font-size: 1.8em;
margin: 0;
letter-spacing: 2px;
}
.search-bar {
flex: 1;
margin: 10px;
max-width: 500px;
}
.search-bar input {
width: 100%;
padding: 10px;
font-size: 1em;
border-radius: 4px;
border: none;
}
main {
padding: 20px;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 20px;
}
.product-card {
background: white;
border-radius: 6px;
padding: 15px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.product-card:hover {
transform: translateY(-3px);
}
.product-card img {
width: 100%;
height: auto;
border-radius: 5px;
}
.product-card h3 {
margin: 10px 0 5px;
font-size: 1.2em;
}
.product-card p {
margin: 5px 0;
color: #555;
}
.price {
color: #d83535;
font-weight: bold;
font-size: 1.1em;
}
.product-card button {
width: 100%;
padding: 10px;
margin-top: 10px;
background: #ffce3d;
border: none;
border-radius: 4px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.product-card button:hover {
background: #f2b200;
}
footer {
background: #5a4fcf;
color: white;
text-align: center;
padding: 20px;
margin-top: 40px;
}
@media (max-width: 600px) {
.search-bar {
order: 3;
width: 100%;
}
header {
flex-direction: column;
align-items: flex-start;
}
}
</style>
</head>
<body>
<header>
<h1>🌈 にじいろモール</h1>
<div class="search-bar">
<input type="text" placeholder="商品を検索...">
</div>
</header>
<main>
<div class="product-grid">
<div class="product-card">
<a href="product1.html">
<img src="https://via.placeholder.com/240x160" alt="スマートウォッチ">
<h3>スマートウォッチ</h3>
</a>
<p class="price">¥12,800</p>
<p>心拍計測 / 防水 / 通知連携</p>
<button>カートに追加</button>
</div>
<div class="product-card">
<a href="product2.html">
<img src="https://via.placeholder.com/240x160" alt="話題の書籍">
<h3>話題の書籍</h3>
</a>
<p class="price">¥1,540</p>
<p>ベストセラー本</p>
<button>カートに追加</button>
</div>
<div class="product-card">
<a href="product3.html">
<img src="https://via.placeholder.com/240x160" alt="Bluetoothイヤホン">
<h3>Bluetoothイヤホン</h3>
</a>
<p class="price">¥5,990</p>
<p>高音質 / ノイズキャンセリング</p>
<button>カートに追加</button>
</div>
</div>
</main>
<footer>
© 2025 にじいろモール - すべての権利を保有します。
</footer>
</body>
</html>
キャラ別ランダムセリフメーカー
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>キャラ別ランダムセリフメーカー</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(to right, #eef2f3, #8e9eab);
text-align: center;
padding: 50px;
}
h1 {
font-size: 30px;
color: #2c3e50;
}
select, button {
padding: 10px;
font-size: 16px;
margin: 10px;
border-radius: 8px;
}
button {
background-color: #2980b9;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: #1f6391;
}
.quote-box {
background: #fff;
margin-top: 30px;
padding: 30px;
border-radius: 12px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
font-size: 20px;
color: #333;
}
</style>
</head>
<body>
<h1>🎙️ キャラ別ランダムセリフメーカー</h1>
<p>ジャンルとキャラクターを選んで、名セリフを生成しよう!</p>
<select id="genre">
<option value="battle">バトル</option>
<option value="romance">恋愛</option>
<option value="drama">感動</option>
<option value="comedy">ギャグ</option>
</select>
<select id="character">
<option value="主人公">主人公</option>
<option value="ヒロイン">ヒロイン</option>
<option value="ライバル">ライバル</option>
<option value="師匠">師匠</option>
</select>
<br>
<button onclick="generateLine()">セリフを生成</button>
<div class="quote-box" id="quote">
ここにセリフが表示されます。
</div>
<script>
const lines = {
battle: {
主人公: [
"俺が倒さなきゃ、誰がやる!",
"まだ…終わっちゃいない!",
"立てるさ、何度でも!"
],
ヒロイン: [
"私だって…守れるんだから!",
"あなたを信じる、それが私の戦いよ。"
],
ライバル: [
"俺を超えてみろ…できるならな!",
"この一撃で、全てを終わらせる。"
],
師匠: [
"強さとは、心にあるものだ。",
"お前にすべてを託す!"
]
},
romance: {
主人公: [
"君に出会うために、生まれてきた気がする。",
"一緒に笑えるだけで、幸せなんだ。"
],
ヒロイン: [
"好きって、こんなにも苦しいの?",
"…バカ。でも、ありがとう。"
],
ライバル: [
"…なぜあいつなんだ?俺じゃ、だめなのか。",
"奪ってでも、お前を手に入れたい。"
],
師匠: [
"愛とは、時に強さよりも難しい。",
"惚れた弱みってやつだな…"
]
},
drama: {
主人公: [
"俺たちは、ただ幸せになりたかっただけなんだ…。",
"運命なんかに、負けてたまるか!"
],
ヒロイン: [
"もう一度…あなたに会いたい。",
"願いが一つだけ叶うなら、時間を戻したい。"
],
ライバル: [
"俺の存在に意味なんてない…と思ってた。",
"あの時の俺を、殴り飛ばしてやりたいよ。"
],
師匠: [
"選んだ道を信じろ。お前なら、やれる。",
"迷っていい。人間なんだからな。"
]
},
comedy: {
主人公: [
"いや、なんでパンツが空飛んでるんだ!?",
"オレの人生、どこで間違えた?"
],
ヒロイン: [
"あーもう!恥ずかしくて死ぬ!!",
"だから言ったでしょ!?ネコじゃないってば!"
],
ライバル: [
"笑うな!こっちは本気なんだぞ!?",
"俺がボケ担当じゃないって言ってるだろ!"
],
師匠: [
"ふぉっふぉっふ、若いのぅ…わしも昔はな…。",
"今日の修行は…温泉じゃ!"
]
}
};
function generateLine() {
const genre = document.getElementById('genre').value;
const character = document.getElementById('character').value;
const options = lines[genre][character];
const line = options[Math.floor(Math.random() * options.length)];
document.getElementById("quote").innerText = `${character}「${line}」`;
}
</script>
</body>
</html>
GSAP Todoリスト
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>GSAP Todoリスト</title>
<style>
body {
font-family: Arial, sans-serif;
background: #f0f8ff;
padding: 20px;
}
#todo-container {
max-width: 500px;
margin: auto;
}
input[type="text"] {
width: 70%;
padding: 10px;
font-size: 16px;
}
button {
padding: 10px;
font-size: 16px;
margin-left: 5px;
cursor: pointer;
}
ul {
list-style: none;
padding: 0;
margin-top: 20px;
}
li {
background: #ffffff;
margin-bottom: 10px;
padding: 10px;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div id="todo-container">
<h2>GSAP Todoリスト</h2>
<input type="text" id="task-input" placeholder="タスクを入力...">
<button onclick="addTask()">追加</button>
<ul id="task-list"></ul>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script>
function addTask() {
const input = document.getElementById("task-input");
const task = input.value.trim();
if (task === "") return;
const li = document.createElement("li");
li.innerHTML = `
<span>${task}</span>
<button onclick="removeTask(this)">削除</button>
`;
document.getElementById("task-list").appendChild(li);
// GSAP アニメーション(フェードイン)
gsap.from(li, {opacity: 0, y: -20, duration: 0.5});
input.value = "";
}
function removeTask(button) {
const li = button.parentElement;
gsap.to(li, {
opacity: 0,
y: 20,
duration: 0.4,
onComplete: () => li.remove()
});
}
</script>
</body>
</html>
Pythonの基礎
Pythonの基礎まとめ
1. 変数とデータ型
x = 10 # 整数(int)
y = 3.14 # 小数(float)
name = "Alice" # 文字列(str)
is_ok = True # 論理値(bool)
2. リスト・辞書
# リスト(配列のようなもの)
fruits = ["apple", "banana", "cherry"]
print(fruits[0]) # → "apple"
# 辞書(キーと値のセット)
person = {"name": "Alice", "age": 20}
print(person["name"]) # → "Alice"
3. if 文(条件分岐)
age = 18
if age >= 20:
print("大人")
else:
print("未成年")
4. for文・while文(繰り返し)
# for文
for fruit in fruits:
print(fruit)
# while文
i = 0
while i < 3:
print(i)
i += 1
5. 関数
def greet(name):
print("Hello, " + name)
greet("Alice") # → Hello, Alice
6. クラス(オブジェクト指向)
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print(self.name + " says ワン!")
dog = Dog("Pochi")
dog.bark() # → Pochi says ワン!
7. モジュールの使い方
import math
print(math.sqrt(16)) # → 4.0
