<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>YESキリスト BOT</title>
<!-- Tailwind(CDN) -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome(アイコン) -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
<meta name="description" content="YESキリストBOT:優しく背中を押してくれるシンプルなチャットボット。今日の励まし、進むべき?などを相談できます。" />
<style>
/* スクロールバー控えめ */
* { scrollbar-width: thin; scrollbar-color: #cbd5e1 transparent; }
*::-webkit-scrollbar { height: 8px; width: 8px; }
*::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; }
/* バブルの三角 */
.bubble:after{
content:""; position:absolute; bottom:-6px; left:16px; border:6px solid transparent; border-top-color:rgba(255,255,255,0.9);
filter: drop-shadow(0 1px 0 rgba(15,23,42,.05));
}
.bubble.me:after{ left:auto; right:16px; border-top-color:#dcfce7; }
</style>
</head>
<body class="min-h-screen bg-gradient-to-b from-slate-50 to-white text-slate-800">
<!-- コンテナ -->
<div class="mx-auto max-w-3xl px-4 py-6">
<!-- ヘッダー -->
<header class="flex items-center justify-between rounded-2xl bg-white/90 backdrop-blur shadow-sm p-4">
<div class="flex items-center gap-3">
<div class="h-10 w-10 rounded-full bg-emerald-500 text-white grid place-items-center">
<i class="fa-solid fa-dove"></i>
</div>
<div>
<h1 class="text-xl font-bold">YESキリスト BOT</h1>
<p class="text-xs text-slate-500">優しく「YES」で背中を押すチャット</p>
</div>
</div>
<div class="flex items-center gap-2">
<button id="btnClear" class="text-xs px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 transition">
履歴クリア
</button>
<button id="btnExport" class="text-xs px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 transition">
エクスポート
</button>
<label class="text-xs px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 transition cursor-pointer">
インポート<input id="fileImport" type="file" accept="application/json" class="hidden">
</label>
</div>
</header>
<!-- プリセット -->
<section class="mt-4 grid grid-cols-2 sm:grid-cols-4 gap-2">
<button class="preset chip">今日の励まし</button>
<button class="preset chip">挑戦していい?</button>
<button class="preset chip">許してもいい?</button>
<button class="preset chip">進むべき?</button>
</section>
<!-- チャット -->
<main id="chat" class="mt-4 h-[60vh] overflow-y-auto rounded-2xl bg-white/90 backdrop-blur p-4 shadow-sm space-y-4">
<!-- 初期メッセージ -->
</main>
<!-- 入力欄 -->
<form id="composer" class="mt-4 flex items-end gap-2">
<textarea id="input" rows="1" placeholder="ここに相談を書いてね(例:新しいことに挑戦しても大丈夫?)"
class="flex-1 resize-none rounded-2xl border border-slate-200 bg-white p-3 focus:outline-none focus:ring-2 focus:ring-emerald-300"></textarea>
<button id="btnSend" type="submit" class="h-11 px-4 rounded-2xl bg-emerald-500 text-white hover:bg-emerald-600 transition">
<i class="fa-solid fa-paper-plane"></i>
</button>
</form>
<!-- 使い方 -->
<details class="mt-4 rounded-2xl bg-slate-50 p-4 text-sm text-slate-600">
<summary class="cursor-pointer font-semibold">使い方</summary>
<ul class="list-disc pl-5 mt-2 space-y-1">
<li>メッセージを送ると、YESキリストが優しく背中を押す言葉で返します。</li>
<li><code>/prayer</code> で短いお祈り風メッセージ、<code>/bless</code> で祝福文。</li>
<li>履歴はブラウザに保存されます(ローカルのみ)。</li>
</ul>
</details>
</div>
<script>
// ====== 設定 ======
const STORAGE_KEY = 'yeschrist_history_v1';
// YESキリストの返答テンプレ
const YES_OPENERS = [
"あなたの心に、静かなYESが灯っています。",
"恐れずに、やさしいYESで一歩を。",
"迷いの中にいても、大丈夫。答えはYESです。",
"小さな信頼が、大きなYESへと育ちます。",
"あなたの良き思いに、YESを重ねましょう。"
];
const YES_ENCOURAGE = [
"試みは愛によって導かれ、愛は前進にYESと言います。",
"完全でなくていい。歩き出す勇気にYES。",
"扉は叩く者に開かれます。ノックにYES。",
"あなたの賜物は隠さずに、光の下へ。YES。",
"やさしさを選ぶ度に、道は明るくなります。YES。"
];
const YES_TAGS = [
"平安がありますように。",
"あなたは一人ではありません。",
"今日の小さな一歩を大切に。",
"心に光を。",
"祝福とともに。"
];
const PRAYERS = [
"天のやさしさがあなたを包み、歩みを照らしますように。アーメン。",
"弱さのときにこそ力が満ちますように。アーメン。",
"迷う心に静けさが与えられますように。アーメン。"
];
const BLESS = [
"あなたの決断に平安が伴いますように。",
"出るにも入るにも祝福が満ちますように。",
"今日の働きに恵みがありますように。"
];
// ====== DOM ======
const chat = document.getElementById('chat');
const input = document.getElementById('input');
const form = document.getElementById('composer');
const btnSend = document.getElementById('btnSend');
const btnExport = document.getElementById('btnExport');
const btnClear = document.getElementById('btnClear');
const fileImport = document.getElementById('fileImport');
document.querySelectorAll('.preset').forEach(el => el.classList.add(
'px-3','py-2','rounded-xl','bg-emerald-50','text-emerald-700','hover:bg-emerald-100','transition','text-sm','chip'
));
// ====== ユーティリティ ======
const nowStr = () => new Date().toLocaleString();
const rand = arr => arr[Math.floor(Math.random() * arr.length)];
const saveHistory = () => {
const items = [...chat.querySelectorAll('[data-msg]')].map(el => ({
role: el.dataset.role, text: el.dataset.msg, time: el.dataset.time
}));
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
};
const loadHistory = () => {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
try { return JSON.parse(raw); } catch { return []; }
};
function appendMessage(role, text, time = nowStr()) {
const wrapper = document.createElement('div');
wrapper.className = role === 'user'
? 'flex justify-end'
: 'flex justify-start';
const bubble = document.createElement('div');
bubble.className = 'relative max-w-[85%] rounded-2xl p-3 bubble shadow-sm ' +
(role === 'user' ? 'bg-emerald-100 me' : 'bg-white/90');
bubble.textContent = text;
const meta = document.createElement('div');
meta.className = 'mt-1 text-[10px] text-slate-500 ' + (role === 'user' ? 'text-right' : 'text-left');
meta.textContent = time;
const container = document.createElement('div');
container.dataset.msg = text;
container.dataset.role = role;
container.dataset.time = time;
container.className = 'space-y-1';
container.appendChild(bubble);
container.appendChild(meta);
wrapper.appendChild(container);
chat.appendChild(wrapper);
chat.scrollTop = chat.scrollHeight;
}
function systemWelcome() {
appendMessage('assistant',
'ようこそ。YESキリストは、あなたの良き願いに「YES」で寄り添います。/prayer で短いお祈り、/bless で祝福文が届きます。');
}
function composeYesReply(userText) {
const lower = (userText || '').toLowerCase();
let opener = rand(YES_OPENERS);
let body = rand(YES_ENCOURAGE);
let tag = rand(YES_TAGS);
// ほんの少しだけ文脈スパイス
if (/[??]$/.test(userText)) {
opener = "その問いかけに、穏やかなYESが返っています。";
}
if (/(許|ゆる)す/.test(userText)) {
body = "赦しは心を自由にし、あなたを前へ押し出します。YES。";
}
if (/(挑戦|チャレンジ|challenge)/i.test(userText)) {
body = "小さくとも踏み出す一歩は尊く、次の景色を連れてきます。YES。";
}
if (/(進|やめ|辞め|やる|やら)/.test(userText)) {
tag = "平安のあるほうへ。YES。";
}
return `${opener}\n${body}\n${tag}`;
}
async function reply(userText) {
// コマンド
if (userText.trim().startsWith('/prayer')) {
appendMessage('assistant', rand(PRAYERS));
saveHistory(); return;
}
if (userText.trim().startsWith('/bless')) {
appendMessage('assistant', rand(BLESS));
saveHistory(); return;
}
// YES返答
const thinking = document.createElement('div');
thinking.className = 'text-xs text-slate-500';
thinking.textContent = '…考えています';
chat.appendChild(thinking); chat.scrollTop = chat.scrollHeight;
await new Promise(r => setTimeout(r, 300)); // 小さな演出
thinking.remove();
appendMessage('assistant', composeYesReply(userText));
saveHistory();
}
// ====== イベント ======
form.addEventListener('submit', async (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
appendMessage('user', text);
input.value = '';
input.style.height = '44px';
saveHistory();
reply(text);
});
// 自動リサイズ
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 160) + 'px';
});
// プリセット
document.querySelectorAll('.preset').forEach(btn => {
btn.addEventListener('click', () => {
const q = btn.textContent.trim();
appendMessage('user', q);
saveHistory();
reply(q);
});
});
// クリア
btnClear.addEventListener('click', () => {
if (!confirm('履歴をすべて削除しますか?')) return;
localStorage.removeItem(STORAGE_KEY);
chat.innerHTML = '';
systemWelcome();
});
// エクスポート
btnExport.addEventListener('click', () => {
const data = localStorage.getItem(STORAGE_KEY) ?? '[]';
const blob = new Blob([data], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `yeschrist_history_${Date.now()}.json`;
a.click();
URL.revokeObjectURL(a.href);
});
// インポート
fileImport.addEventListener('change', async (e) => {
const file = e.target.files?.[0];
if (!file) return;
const text = await file.text();
try {
const arr = JSON.parse(text);
if (!Array.isArray(arr)) throw new Error('format');
localStorage.setItem(STORAGE_KEY, text);
chat.innerHTML = '';
arr.forEach(m => appendMessage(m.role, m.text, m.time));
} catch {
alert('インポート失敗:JSON形式が正しくありません。');
} finally {
fileImport.value = '';
}
});
// ====== 初期化 ======
(function init() {
const hist = loadHistory();
if (hist.length === 0) {
systemWelcome();
} else {
hist.forEach(m => appendMessage(m.role, m.text, m.time));
}
// 入力高さ初期
input.style.height = '44px';
})();
</script>
</body>
</html>