ホロライブAIプロンプトサイト.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>ホロライブ AIプロンプト生成サイト</title>
  <meta name="description" content="ホロライブのメンバー向けに、画像生成AIで使えるプロンプトをワンクリック作成。日英対応/テンプレ/ネガティブ/コピーボタン/履歴保存。" />
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700;900&display=swap" rel="stylesheet">
  <script src="https://cdn.tailwindcss.com"></script>
  <script>
    tailwind.config = {
      theme: {
        extend: {
          fontFamily: { sans: ['Noto Sans JP', 'ui-sans-serif', 'system-ui'] },
          colors: {
            skin: {
              base: '#0b1020',
              card: '#0f152b',
              accent: '#60a5fa',
              soft: '#a5b4fc'
            }
          },
          boxShadow: {
            glass: '0 8px 30px rgba(0,0,0,.35)'
          }
        }
      }
    }
  </script>
  <style>
    html,body{height:100%}
    .glass{backdrop-filter: saturate(140%) blur(12px); background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));}
    .chip{border:1px solid rgba(255,255,255,.15);}
    .mono{font-feature-settings: "ss01" on, "cv01" on;}
  </style>
</head>
<body class="min-h-screen bg-gradient-to-br from-slate-900 via-skin-base to-black text-slate-100 font-sans">
  <header class="sticky top-0 z-30 border-b border-white/10 bg-slate-900/70 glass">
    <div class="max-w-6xl mx-auto px-4 py-4 flex items-center gap-4">
      <div class="size-10 rounded-2xl bg-skin-accent/20 grid place-items-center shadow-glass">
        <span class="text-skin-accent font-black">AI</span>
      </div>
      <div>
        <h1 class="text-xl md:text-2xl font-extrabold tracking-tight">ホロライブ AIプロンプト生成</h1>
        <p class="text-slate-300 text-sm">メンバーを選ぶ→テンプレを選ぶ→生成! 日/英・ネガティブ・履歴・コピペ完備</p>
      </div>
      <div class="ms-auto flex items-center gap-3">
        <label class="flex items-center gap-2 text-sm"><input id="langToggle" type="checkbox" class="accent-skin-accent"> 英語で出力</label>
        <button id="randomBtn" class="px-3 py-2 rounded-xl bg-white/10 hover:bg-white/20 border border-white/10">ランダム</button>
        <button id="resetBtn" class="px-3 py-2 rounded-xl bg-white/10 hover:bg-white/20 border border-white/10">リセット</button>
      </div>
    </div>
  </header>

  <main class="max-w-6xl mx-auto px-4 py-6 grid lg:grid-cols-2 gap-6">
    <!-- 左:入力パネル -->
    <section class="glass rounded-2xl p-5 shadow-glass border border-white/10">
      <h2 class="font-bold text-lg mb-3">1) メンバー & スタイル設定</h2>
      <div class="grid md:grid-cols-2 gap-4">
        <div>
          <label class="block text-sm mb-1">メンバー</label>
          <div class="flex gap-2">
            <input id="memberSearch" type="text" placeholder="名前/世代/特徴で検索" class="w-full px-3 py-2 rounded-xl bg-black/30 border border-white/10" />
          </div>
          <div class="mt-2 max-h-48 overflow-auto pr-1">
            <ul id="memberList" class="space-y-1"></ul>
          </div>
        </div>
        <div>
          <label class="block text-sm mb-1">テンプレート (用途)</label>
          <select id="templateSelect" class="w-full px-3 py-2 rounded-xl bg-black/30 border border-white/10">
            <option value="portrait">高品質ポートレート</option>
            <option value="fullbody">全身イラスト</option>
            <option value="chibi">デフォルメ/ちびキャラ</option>
            <option value="vtuber">VTuber配信サムネ</option>
            <option value="live2d">Live2D立ち絵</option>
            <option value="vrchat">VRChat アバター風</option>
            <option value="manga">モノクロ漫画コマ</option>
            <option value="poster">キービジュアル/ポスター</option>
            <option value="landscape">背景&小さめ人物</option>
          </select>
          <div class="grid grid-cols-2 gap-2 mt-3">
            <label class="text-sm flex items-center gap-2"><input id="nsfwSafe" type="checkbox" checked class="accent-skin-accent"> NSFW禁止</label>
            <label class="text-sm flex items-center gap-2"><input id="useNeg" type="checkbox" checked class="accent-skin-accent"> ネガティブ付与</label>
            <label class="text-sm flex items-center gap-2"><input id="addPose" type="checkbox" class="accent-skin-accent"> ポーズ指定</label>
            <label class="text-sm flex items-center gap-2"><input id="addCamera" type="checkbox" class="accent-skin-accent"> カメラ/レンズ</label>
          </div>
        </div>
      </div>

      <div class="grid md:grid-cols-3 gap-4 mt-5">
        <div>
          <label class="block text-sm mb-1">画風プリセット</label>
          <select id="stylePreset" class="w-full px-3 py-2 rounded-xl bg-black/30 border border-white/10">
            <option value="anime">アニメ塗り(鮮やか)</option>
            <option value="semiReal">セミリアル</option>
            <option value="watercolor">水彩/やわらか</option>
            <option value="celshade">セルルック</option>
            <option value="painterly">厚塗り/絵画風</option>
            <option value="3dtoon">3Dトゥーン</option>
          </select>
        </div>
        <div>
          <label class="block text-sm mb-1">照明/雰囲気</label>
          <select id="moodPreset" class="w-full px-3 py-2 rounded-xl bg-black/30 border border-white/10">
            <option value="soft">柔らかい光 / やさしい雰囲気</option>
            <option value="dramatic">ドラマチック / リムライト</option>
            <option value="studio">スタジオ照明 / クリーン</option>
            <option value="sunset">夕焼け / ゴールデンアワー</option>
            <option value="night">夜景 / ネオン</option>
          </select>
        </div>
        <div>
          <label class="block text-sm mb-1">解像度・比率</label>
          <select id="aspectPreset" class="w-full px-3 py-2 rounded-xl bg-black/30 border border-white/10">
            <option value="square">正方形(1024×1024)</option>
            <option value="portrait">縦長(768×1152)</option>
            <option value="landscape">横長(1152×768)</option>
            <option value="thumb">サムネ(1280×720)</option>
          </select>
        </div>
      </div>

      <div class="mt-5 grid md:grid-cols-2 gap-4">
        <div>
          <label class="block text-sm mb-1">衣装・小物(任意)</label>
          <input id="outfitInput" type="text" placeholder="例: 制服, ライブ衣装, 私服, 王冠, マント" class="w-full px-3 py-2 rounded-xl bg-black/30 border border-white/10" />
        </div>
        <div>
          <label class="block text-sm mb-1">背景・シーン(任意)</label>
          <input id="bgInput" type="text" placeholder="例: ステージ, 星空, 教室, サイバーシティ" class="w-full px-3 py-2 rounded-xl bg-black/30 border border-white/10" />
        </div>
      </div>

      <div class="mt-5 grid md:grid-cols-2 gap-4">
        <div>
          <label class="block text-sm mb-1">モデル/LoRA(任意)</label>
          <input id="modelInput" type="text" placeholder="例: anime-v4, AnythingV5, holo_member_lora:0.8" class="w-full px-3 py-2 rounded-xl bg-black/30 border border-white/10" />
        </div>
        <div>
          <label class="block text-sm mb-1">追加キーワード(任意)</label>
          <input id="extraInput" type="text" placeholder="例: 1girl, detailed eyes, dynamic lighting" class="w-full px-3 py-2 rounded-xl bg-black/30 border border-white/10" />
        </div>
      </div>

      <div class="mt-6 flex flex-wrap gap-3">
        <button id="generateBtn" class="px-5 py-3 rounded-2xl bg-skin-accent text-black font-bold hover:opacity-90">生成</button>
        <button id="copyBtn" class="px-5 py-3 rounded-2xl bg-white/10 border border-white/10 hover:bg-white/20">コピー</button>
        <button id="saveBtn" class="px-5 py-3 rounded-2xl bg-white/10 border border-white/10 hover:bg-white/20">履歴に保存</button>
        <button id="exportBtn" class="px-5 py-3 rounded-2xl bg-white/10 border border-white/10 hover:bg-white/20">JSON書き出し</button>
        <button id="importBtn" class="px-5 py-3 rounded-2xl bg-white/10 border border-white/10 hover:bg-white/20">JSON読込</button>
      </div>
    </section>

    <!-- 右:出力/履歴 -->
    <section class="space-y-6">
      <div class="glass rounded-2xl p-5 shadow-glass border border-white/10">
        <h2 class="font-bold text-lg">2) 出力(Positive / Negative / メタ)</h2>
        <div class="mt-3 grid gap-3">
          <label class="text-sm">Positive Prompt</label>
          <textarea id="posOut" rows="6" class="mono w-full px-3 py-2 rounded-xl bg-black/50 border border-white/10" placeholder="ここに生成結果"></textarea>
          <label class="text-sm">Negative Prompt</label>
          <textarea id="negOut" rows="4" class="mono w-full px-3 py-2 rounded-xl bg-black/50 border border-white/10" placeholder="ここにネガティブ"></textarea>
          <div class="grid md:grid-cols-3 gap-3">
            <div>
              <label class="text-sm">解像度</label>
              <input id="metaRes" class="w-full px-3 py-2 rounded-xl bg-black/50 border border-white/10" readonly>
            </div>
            <div>
              <label class="text-sm">推奨CFG/Steps</label>
              <input id="metaCfg" class="w-full px-3 py-2 rounded-xl bg-black/50 border border-white/10" value="CFG 6-8 / Steps 28-36">
            </div>
            <div>
              <label class="text-sm">推奨Sampler</label>
              <input id="metaSampler" class="w-full px-3 py-2 rounded-xl bg-black/50 border border-white/10" value="DPM++ 2M Karras">
            </div>
          </div>
        </div>
      </div>

      <div class="glass rounded-2xl p-5 shadow-glass border border-white/10">
        <div class="flex items-center gap-3 mb-3">
          <h2 class="font-bold text-lg">3) 履歴</h2>
          <button id="clearHist" class="ms-auto px-3 py-1.5 text-sm rounded-xl bg-white/10 border border-white/10 hover:bg-white/20">全削除</button>
        </div>
        <div id="history" class="space-y-3 max-h-72 overflow-auto pr-1"></div>
      </div>
    </section>
  </main>

  <footer class="max-w-6xl mx-auto px-4 pb-10 text-slate-400 text-sm">
    <div class="glass rounded-2xl p-4 border border-white/10">
      <p class="leading-relaxed">注意:本ツールは各メンバーの公式ガイドラインを尊重し、成人向けや誹謗中傷の内容を禁止します。商用利用や二次創作ルールは各社ポリシーをご確認ください。</p>
    </div>
  </footer>

  <script>
    // --- データセット(抜粋・追加可) ---
    const MEMBERS = [
      // JP
      { key:'Tokino Sora', gen:'JP0', color:'#5bc0eb', motifs:['星','リボン'], traits:['清楚','やさしい'], outfit:['セーラー風','リボン'], keywords:['idol','first gen','blue ribbon'], en:true },
      { key:'Shirakami Fubuki', gen:'JP1', color:'#ffffff', motifs:['狐','尻尾'], traits:['元気','明るい'], outfit:['セーラー','マフラー'], keywords:['fox girl','white hair','animal ears'] },
      { key:'Natsuiro Matsuri', gen:'JP1', color:'#f4a261', motifs:['祭','ポニーテール'], traits:['活発','いたずら'], outfit:['体操服','浴衣'], keywords:['cheerful','ponytail'] },
      { key:'Minato Aqua', gen:'JP2', color:'#b388ff', motifs:['メイド','ヘッドドレス'], traits:['ドジっ子','ピンク紫髪'], outfit:['メイド服'], keywords:['maid','twin tails'] },
      { key:'Shion', gen:'JP2', color:'#c084fc', motifs:['魔法','三角帽'], traits:['小悪魔','ツリ目'], outfit:['魔女服'], keywords:['witch','purple hair'] },
      { key:'Nakiri Ayame', gen:'JP2', color:'#ef4444', motifs:['鬼角','和装'], traits:['クール','凛'], outfit:['巫女風'], keywords:['oni horns','kimono style'] },
      { key:'Ookami Mio', gen:'GAMERS', color:'#111827', motifs:['狼','耳'], traits:['頼れる','落ち着き'], outfit:['黒衣装'], keywords:['wolf girl','black outfit'] },
      { key:'Houshou Marine', gen:'JP3', color:'#ef4444', motifs:['海賊帽','錨'], traits:['情熱','大人っぽい'], outfit:['海賊衣装'], keywords:['pirate','captain hat'] },
      { key:'Usada Pekora', gen:'JP3', color:'#93c5fd', motifs:['うさ耳','人参'], traits:['やんちゃ','元気'], outfit:['うさぎパーカー'], keywords:['bunny ears','carrot'] },
      { key:'Shiranui Flare', gen:'JP3', color:'#f59e0b', motifs:['エルフ','耳'], traits:['包容','陽気'], outfit:['冒険者'], keywords:['elf ears','adventurer'] },
      { key:'Shirogane Noel', gen:'JP3', color:'#9ca3af', motifs:['騎士','鎧'], traits:['真面目','力持ち'], outfit:['鎧','マント'], keywords:['knight armor','silver hair'] },
      { key:'Hoshimachi Suisei', gen:'INoNaka/JP', color:'#60a5fa', motifs:['星','アイドル'], traits:['クール','アイドル'], outfit:['青系衣装'], keywords:['star motif','blue idol'] },
      { key:'Amane Kanata', gen:'JP4', color:'#60a5fa', motifs:['天使','羽'], traits:['ストイック'], outfit:['白蒼衣装'], keywords:['angel wings','halo'] },
      { key:'Kiryu Coco', gen:'JP4', color:'#f97316', motifs:['ドラゴン','角'], traits:['豪快'], outfit:['ドラゴンモチーフ'], keywords:['dragon horns','orange hair'] },
      { key:'Tsunomaki Watame', gen:'JP4', color:'#facc15', motifs:['羊','リボン'], traits:['ふわふわ'], outfit:['羊モチーフ'], keywords:['sheep girl','blonde'] },
      { key:'Himemori Luna', gen:'JP4', color:'#f472b6', motifs:['姫','王冠'], traits:['キュート'], outfit:['姫ドレス'], keywords:['princess crown','pink'] },
      { key:'Laplus Darknesss', gen:'HoloX', color:'#6d28d9', motifs:['悪魔','マント'], traits:['いたずら'], outfit:['黒紫コート'], keywords:['devilish','hooded coat'] },
      { key:'Takane Lui', gen:'HoloX', color:'#ef4444', motifs:['スパイ','赤黒'], traits:['クール'], outfit:['スーツ風'], keywords:['spy','red black'] },
      { key:'Sakamata Chloe', gen:'HoloX', color:'#94a3b8', motifs:['シャチ','フード'], traits:['あざとい'], outfit:['白黒フード'], keywords:['orca hoodie','monochrome'] },
      { key:'Hakui Koyori', gen:'HoloX', color:'#fb7185', motifs:['研究','ピンク'], traits:['好奇心'], outfit:['研究白衣'], keywords:['lab coat','pink hair'] },
      { key:'Kazama Iroha', gen:'HoloX', color:'#86efac', motifs:['忍者','刀'], traits:['素直'], outfit:['忍装束'], keywords:['ninja','katana'] },
      // EN
      { key:'Mori Calliope', gen:'EN Myth', color:'#ef4444', motifs:['鎌','死神'], traits:['クール'], outfit:['黒×ピンク'], keywords:['reaper scythe','rapper'] },
      { key:'Gawr Gura', gen:'EN Myth', color:'#60a5fa', motifs:['サメ','フード'], traits:['いたずら'], outfit:['サメパーカー'], keywords:['shark hoodie','trident'] },
      { key:'Takanashi Kiara', gen:'EN Myth', color:'#fb923c', motifs:['鳥','オレンジ'], traits:['情熱'], outfit:['アイドル衣装'], keywords:['phoenix','orange hair'] },
      { key:"Ninomae Ina'nis", gen:'EN Myth', color:'#a78bfa', motifs:['触手','本'], traits:['穏やか'], outfit:['修道服風'], keywords:['tentacle motif','violet'] },  <!-- ★ 修正済み:ダブルクォート -->
      { key:'Amelia Watson', gen:'EN Myth', color:'#fbbf24', motifs:['探偵','時計'], traits:['好奇心'], outfit:['探偵コート'], keywords:['detective','magnifying glass'] },
      { key:'Hakos Baelz', gen:'EN Council', color:'#ef4444', motifs:['ネズミ','カオス'], traits:['ハイテンション'], outfit:['赤系衣装'], keywords:['chaos','rat tail'] },
      { key:'IRyS', gen:'EN Project:Hope', color:'#ef5fff', motifs:['天使悪魔','クリスタル'], traits:['希望'], outfit:['黒×赤×紫'], keywords:['nephilim','crystal'] },
      // ID (抜粋)
      { key:'Kobo Kanaeru', gen:'ID3', color:'#60a5fa', motifs:['雨','水'], traits:['やんちゃ'], outfit:['青系パーカー'], keywords:['rain theme','blue hair'] },
    ];

    // ネガティブテンプレ
    const NEGATIVE = 'nsfw, nude, lowres, low quality, worst quality, extra fingers, deformed hands, poorly drawn, watermark, logo, signature, text, blurry, jpeg artifacts, bad anatomy, out of frame';

    // UI取得
    const memberSearch = document.getElementById('memberSearch');
    const memberList = document.getElementById('memberList');
    const templateSelect = document.getElementById('templateSelect');
    const stylePreset = document.getElementById('stylePreset');
    const moodPreset = document.getElementById('moodPreset');
    const aspectPreset = document.getElementById('aspectPreset');
    const outfitInput = document.getElementById('outfitInput');
    const bgInput = document.getElementById('bgInput');
    const modelInput = document.getElementById('modelInput');
    const extraInput = document.getElementById('extraInput');
    const langToggle = document.getElementById('langToggle');
    const nsfwSafe = document.getElementById('nsfwSafe');
    const useNeg = document.getElementById('useNeg');
    const addPose = document.getElementById('addPose');
    const addCamera = document.getElementById('addCamera');

    const generateBtn = document.getElementById('generateBtn');
    const copyBtn = document.getElementById('copyBtn');
    const saveBtn = document.getElementById('saveBtn');
    const exportBtn = document.getElementById('exportBtn');
    const importBtn = document.getElementById('importBtn');
    const randomBtn = document.getElementById('randomBtn');
    const resetBtn = document.getElementById('resetBtn');

    const posOut = document.getElementById('posOut');
    const negOut = document.getElementById('negOut');
    const metaRes = document.getElementById('metaRes');
    const metaCfg = document.getElementById('metaCfg');
    const metaSampler = document.getElementById('metaSampler');
    const history = document.getElementById('history');
    const clearHist = document.getElementById('clearHist');

    // 状態
    let selectedMember = null;

    // 初期描画
    function renderMembers(filter=''){
      const f = filter.toLowerCase().trim();
      memberList.innerHTML = '';
      MEMBERS.filter(m=>{
        const s = [m.key, m.gen, ...(m.motifs||[]), ...(m.traits||[]), ...(m.keywords||[])].join(' ').toLowerCase();
        return !f || s.includes(f);
      }).forEach(m=>{
        const li = document.createElement('li');
        li.className = 'chip rounded-xl px-3 py-2 flex items-center gap-2 hover:bg-white/5 cursor-pointer';
        li.innerHTML = `<span class="inline-block size-3 rounded-full" style="background:${m.color}"></span><span class="font-medium">${m.key}</span><span class="text-xs text-slate-400">(${m.gen})</span>`;
        // クリック選択
        li.addEventListener('click',()=>{ selectedMember = m; highlightSelection(li); });
        // ★ キーボード対応
        li.setAttribute('tabindex', '0');
        li.setAttribute('role', 'button');
        li.addEventListener('keydown', (e) => {
          if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            selectedMember = m;
            highlightSelection(li);
          }
        });
        // 既選択のハイライト維持
        if (selectedMember && selectedMember.key === m.key) li.classList.add('bg-white/10');
        memberList.appendChild(li);
      })
    }

    function highlightSelection(activeLi){
      [...memberList.children].forEach(li=> li.classList.remove('bg-white/10'));
      activeLi.classList.add('bg-white/10');
    }

    memberSearch.addEventListener('input', e=> renderMembers(e.target.value));

    // アスペクト → 表示解像度
    function applyAspect(){
      const map = {
        square: '1024x1024',
        portrait: '768x1152',
        landscape: '1152x768',
        thumb: '1280x720'
      };
      metaRes.value = map[aspectPreset.value] || '1024x1024';
    }

    aspectPreset.addEventListener('change', applyAspect);
    applyAspect();

    // テンプレ文
    function templateText(id, name){
      const jp = {
        portrait: `${name}の高品質なバストアップポートレート, 視線はこちら, きらめく瞳, 細密な髪, 肌の質感,`,
        fullbody: `${name}の全身イラスト, ダイナミックなポーズ,`,
        chibi: `${name}のデフォルメちびキャラ, 等身2~3,`,
        vtuber: `${name}の配信サムネ風イラスト, サイドライト, 目を引くタイポの余白,`,
        live2d: `${name}のLive2D立ち絵, 胸上〜腰上, レイヤー分けしやすいシンプル背景,`,
        vrchat: `${name}のVRChatアバター風デザイン, 全身, セルルック,`,
        manga: `${name}が登場するモノクロ漫画コマ, スクリーントーン,`,
        poster: `${name}のキービジュアル, 迫力のある構図,`,
        landscape: `背景美術の中に小さく${name}, 遠景, 雰囲気重視,`
      };
      const en = {
        portrait: `high-quality bust portrait of ${name}, looking at viewer, sparkling eyes, detailed hair, skin texture,`,
        fullbody: `full-body illustration of ${name}, dynamic pose,`,
        chibi: `super-deformed chibi ${name}, 2~3 heads tall,`,
        vtuber: `stream thumbnail style illustration of ${name}, side lighting, space for bold typography,`,
        live2d: `Live2D standing illustration of ${name}, bust to waist-up, simple background for easy layer separation,`,
        vrchat: `VRChat avatar style design of ${name}, full body, toon shading,`,
        manga: `monochrome manga panel featuring ${name}, screen tones,`,
        poster: `key visual poster of ${name}, impactful composition,`,
        landscape: `cinematic background with small ${name} in scene, distant view, mood-focused,`
      }
      return (langToggle.checked? en : jp)[id] || '';
    }

    // スタイル・ムード
    function styleText(id){
      const jp = {
        anime: 'アニメ塗り, 高発色, クリアライン,',
        semiReal: 'セミリアル, 繊細なライティング, 細密質感,',
        watercolor: '水彩風, 柔らかい発色, にじみ,',
        celshade: 'セルシェーディング, シャープな影,',
        painterly: '厚塗り, 筆致, 奥行き,',
        threetoon: '3Dトゥーン, ノンフォトリアル,'
      };
      const en = {
        anime: 'anime coloring, vivid, clean linework,',
        semiReal: 'semi-realistic, delicate lighting, fine textures,',
        watercolor: 'watercolor style, soft colors, bleeding,',
        celshade: 'cel-shaded, sharp shadows,',
        painterly: 'painterly, visible brush strokes, depth,',
        threetoon: '3D toon, non-photorealistic,'
      };
      const key = id === '3dtoon' ? 'threetoon' : id;
      return (langToggle.checked? en : jp)[key] || '';
    }

    function moodText(id){
      const jp = {
        soft: 'やわらかい環境光, 穏やかな表情,',
        dramatic: 'ドラマチックライティング, リムライト, コントラスト強,',
        studio: 'スタジオ照明, 均一な光, 背景シンプル,',
        sunset: '夕焼けの光, ゴールデンアワー,',
        night: '夜景ネオン, グロー, 反射,'
      };
      const en = {
        soft: 'soft ambient light, gentle expression,',
        dramatic: 'dramatic lighting, rim light, high contrast,',
        studio: 'studio lighting, even illumination, simple background,',
        sunset: 'sunset glow, golden hour,',
        night: 'neon nightscape, glow, reflections,'
      };
      return (langToggle.checked? en : jp)[id] || '';
    }

    function poseText(){
      const jp = ['ピースサイン','片手を胸に','ほほえみ','ダンスポーズ','跳躍'];
      const en = ['peace sign','hand on chest','gentle smile','dance pose','jumping'];
      const arr = langToggle.checked? en : jp;
      return arr[Math.floor(Math.random()*arr.length)]
    }

    function cameraText(){
      const jp = ['50mmレンズ相当','f1.8被写界深度','極小ノイズ','シャープ'];
      const en = ['50mm lens equivalent','f1.8 shallow depth of field','very low noise','sharp'];
      const arr = langToggle.checked? en : jp;
      return arr.join(', ');
    }

    function buildPositive(){
      if(!selectedMember){
        alert('メンバーを選択してください');
        return '';
      }
      const name = selectedMember.key;
      const tp = templateText(templateSelect.value, name);
      const st = styleText(stylePreset.value);
      const md = moodText(moodPreset.value);
      const ex = extraInput.value?.trim();
      const pieces = [tp, st, md];

      if(outfitInput.value.trim()) pieces.push(langToggle.checked? `outfit: ${outfitInput.value.trim()}` : `衣装: ${outfitInput.value.trim()}`);
      if(bgInput.value.trim()) pieces.push(langToggle.checked? `background: ${bgInput.value.trim()}` : `背景: ${bgInput.value.trim()}`);
      if(addPose.checked) pieces.push(langToggle.checked? `pose: ${poseText()}` : `ポーズ: ${poseText()}`);
      if(addCamera.checked) pieces.push(cameraText());

      // メンバー特徴
      const motif = (selectedMember.motifs||[]).join(', ');
      const trait = (selectedMember.traits||[]).join(', ');
      const kw = (selectedMember.keywords||[]).join(', ');
      const profJP = `特徴: ${motif}, 性格: ${trait}, キーワード: ${kw}`;
      const profEN = `motifs: ${motif}, traits: ${trait}, keywords: ${kw}`;
      pieces.push(langToggle.checked? profEN : profJP);

      if(ex) pieces.push(ex);

      // モデル/LoRA
      if(modelInput.value.trim()) pieces.push(`[${modelInput.value.trim()}]`);

      // NSFW安全
      if(nsfwSafe.checked){
        pieces.push(langToggle.checked? 'sfw, wholesome' : '全年齢, 健全');
      }

      return pieces.filter(Boolean).join(' ');
    }

    function buildNegative(){
      return useNeg.checked ? NEGATIVE : '';
    }

    function generate(){
      const pos = buildPositive();
      if (!pos) return;
      posOut.value = pos;
      negOut.value = buildNegative();
    }

    function copyAll(){
      const txt = `Positive:\n${posOut.value}\n\nNegative:\n${negOut.value}\n\nMeta:\nres=${metaRes.value}, ${metaCfg.value}, sampler=${metaSampler.value}`;
      navigator.clipboard.writeText(txt).then(()=>{ toast('コピーしました'); });
    }

    function toast(msg){
      const t = document.createElement('div');
      t.textContent = msg;
      t.className = 'fixed bottom-5 left-1/2 -translate-x-1/2 px-4 py-2 rounded-xl bg-white/10 border border-white/10 shadow-glass';
      document.body.appendChild(t);
      setTimeout(()=> t.remove(), 1600);
    }

    function saveHistory(){
      const item = {
        time: new Date().toISOString(),
        member: selectedMember?.key || '(未選択)',
        template: templateSelect.value,
        style: stylePreset.value,
        mood: moodPreset.value,
        aspect: aspectPreset.value,
        pos: posOut.value,
        neg: negOut.value,
        meta: { res: metaRes.value, cfg: metaCfg.value, sampler: metaSampler.value }
      };
      const arr = JSON.parse(localStorage.getItem('holo_prompt_hist')||'[]');
      arr.unshift(item);
      // 保存上限:最新から最大100件(文字列長で切るとJSONが壊れるため件数で制御)
      const MAX_ITEMS = 100;
      localStorage.setItem('holo_prompt_hist', JSON.stringify(arr.slice(0, MAX_ITEMS)));
      renderHistory();
      toast('履歴に保存しました');
    }

    function renderHistory(){
      const arr = JSON.parse(localStorage.getItem('holo_prompt_hist')||'[]');
      history.innerHTML = '';
      arr.forEach((it, idx)=>{
        const card = document.createElement('div');
        card.className = 'rounded-xl p-3 border border-white/10 bg-black/30';
        card.innerHTML = `
          <div class='flex items-center gap-2 mb-2'>
            <span class='text-slate-300 text-sm'>${new Date(it.time).toLocaleString()}</span>
            <span class='ms-auto text-xs chip rounded-lg px-2 py-0.5'>${it.member}</span>
          </div>
          <div class='text-xs text-slate-400 mb-2'>${it.template} / ${it.style} / ${it.mood} / ${it.aspect}</div>
          <details class='mb-2'>
            <summary class='cursor-pointer text-skin-soft'>Positive</summary>
            <pre class='whitespace-pre-wrap text-sm'>${escapeHtml(it.pos)}</pre>
          </details>
          <details class='mb-2'>
            <summary class='cursor-pointer text-skin-soft'>Negative</summary>
            <pre class='whitespace-pre-wrap text-sm'>${escapeHtml(it.neg)}</pre>
          </details>
          <div class='flex gap-2'>
            <button class='px-3 py-1.5 rounded-lg bg-white/10 border border-white/10 hover:bg-white/20' data-act='load' data-idx='${idx}'>読み込む</button>
            <button class='px-3 py-1.5 rounded-lg bg-white/10 border border-white/10 hover:bg-white/20' data-act='copy' data-idx='${idx}'>コピー</button>
            <button class='px-3 py-1.5 rounded-lg bg-white/10 border border-white/10 hover:bg-white/20' data-act='del' data-idx='${idx}'>削除</button>
          </div>
        `;
        card.addEventListener('click', e=>{
          const btn = e.target.closest('button');
          if(!btn) return;
          const { act, idx } = btn.dataset;
          const list = JSON.parse(localStorage.getItem('holo_prompt_hist')||'[]');
          if(act==='del'){
            list.splice(idx,1);
            localStorage.setItem('holo_prompt_hist', JSON.stringify(list));
            renderHistory();
          }else if(act==='copy'){
            navigator.clipboard.writeText(`Positive:\n${list[idx].pos}\n\nNegative:\n${list[idx].neg}`);
            toast('コピーしました');
          }else if(act==='load'){
            loadHistoryItem(list[idx]);
          }
        })
        history.appendChild(card);
      })
    }

    function loadHistoryItem(it){
      selectedMember = MEMBERS.find(m=> m.key === it.member) || null;
      templateSelect.value = it.template;
      stylePreset.value = it.style;
      moodPreset.value = it.mood;
      aspectPreset.value = it.aspect; applyAspect();
      posOut.value = it.pos; negOut.value = it.neg;
      toast('履歴を読み込みました');
      renderMembers(memberSearch.value);
    }

    clearHist.addEventListener('click', ()=>{
      localStorage.removeItem('holo_prompt_hist');
      renderHistory();
      toast('履歴を削除しました');
    });

    // JSON入出力
    exportBtn.addEventListener('click', ()=>{
      const data = localStorage.getItem('holo_prompt_hist')||'[]';
      const blob = new Blob([data], {type:'application/json'});
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = 'holo_prompt_history.json';
      a.click();
    });

    importBtn.addEventListener('click', ()=>{
      const inp = document.createElement('input');
      inp.type = 'file'; inp.accept = 'application/json';
      inp.onchange = () => {
        const file = inp.files[0];
        if(!file) return;
        const reader = new FileReader();
        reader.onload = e => {
          try{
            const arr = JSON.parse(e.target.result);
            if(Array.isArray(arr)){
              localStorage.setItem('holo_prompt_hist', JSON.stringify(arr));
              renderHistory();
              toast('JSONを読み込みました');
            } else { alert('不正なJSONです'); }
          }catch(err){ alert('読み込み失敗: '+err.message); }
        };
        reader.readAsText(file);
      };
      inp.click();
    });

    // ランダム&リセット
    randomBtn.addEventListener('click', ()=>{
      selectedMember = MEMBERS[Math.floor(Math.random()*MEMBERS.length)];
      renderMembers(memberSearch.value);
      generate();
      toast('ランダム選択しました');
    });

    resetBtn.addEventListener('click', ()=>{
      memberSearch.value=''; selectedMember=null; renderMembers('');
      templateSelect.value='portrait'; stylePreset.value='anime'; moodPreset.value='soft'; aspectPreset.value='square'; applyAspect();
      outfitInput.value=''; bgInput.value=''; modelInput.value=''; extraInput.value='';
      posOut.value=''; negOut.value='';
      toast('初期化しました');
    });

    // 生成/コピー/保存
    generateBtn.addEventListener('click', generate);
    copyBtn.addEventListener('click', copyAll);
    saveBtn.addEventListener('click', saveHistory);

    // 言語切替時に再生成
    langToggle.addEventListener('change', ()=>{ if(posOut.value) generate(); });

    // HTMLエスケープ
    function escapeHtml(str=''){
      return str.replace(/[&<>"]/g, s=> ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[s]));
    }

    // 初期描画
    renderMembers('');
    renderHistory();
  </script>
</body>
</html>

投稿者: chosuke

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

コメントを残す

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