YESキリストBOT

<!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>

投稿者: chosuke

趣味はゲームやアニメや漫画などです

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です