HomeMAL' // MANUFACTORUM
datacrypt
AICastle / index.html
index.html54.8 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AICastle</title>
<style>
:root {
  --bg: #12101e;
  --bg2: #1a1730;
  --bg3: #252040;
  --text: #dcd8e8;
  --text-dim: #7e7a92;
  --accent: #d63b5a;
  --accent2: #7c4dff;
  --gold: #e0b44a;
  --input-bg: #1e1b32;
  --border: #2e2a45;
  --green: #43b581;
  --shadow: rgba(0,0,0,0.35);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Georgia', 'Times New Roman', serif; background: var(--bg); color: var(--text); min-height: 100vh; }

.app { display: flex; height: 100vh; }
.sidebar { width: 260px; background: var(--bg2); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; }
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; background: var(--bg); }

.sidebar-header { padding: 20px; border-bottom: 1px solid var(--border); }
.sidebar-header h1 { font-size: 1.4em; color: var(--gold); letter-spacing: 3px; font-variant: small-caps; }
.sidebar-actions { padding: 12px; display: flex; flex-direction: column; gap: 6px; }
.sidebar-actions button { padding: 10px; border: 1px solid var(--border); background: var(--input-bg); color: var(--text); border-radius: 8px; cursor: pointer; font-size: 0.85em; transition: all 0.2s; }
.sidebar-actions button:hover { background: var(--bg3); border-color: var(--accent2); }
.sidebar-actions button.primary { background: linear-gradient(135deg, var(--accent) 0%, #b82e4a 100%); border-color: transparent; color: white; font-weight: bold; }
.sidebar-actions button.primary:hover { filter: brightness(1.15); }
.game-list { flex: 1; overflow-y: auto; padding: 8px; }
.game-item { padding: 12px 14px; border-radius: 8px; cursor: pointer; margin-bottom: 2px; transition: all 0.2s; border-left: 3px solid transparent; }
.game-item:hover { background: var(--bg3); }
.game-item.active { background: var(--bg3); border-left-color: var(--gold); }
.game-item .title { font-weight: bold; font-size: 0.9em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.game-item .meta { font-size: 0.7em; color: var(--text-dim); margin-top: 4px; }

.top-bar { padding: 10px 20px; background: var(--bg2); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
.top-bar .game-title { flex: 1; font-size: 1.05em; color: var(--gold); font-weight: normal; letter-spacing: 0.5px; }
.model-select { padding: 6px 10px; background: var(--input-bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font-size: 0.8em; }
.top-bar button { padding: 6px 14px; background: var(--input-bg); color: var(--text-dim); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-size: 0.8em; transition: all 0.2s; letter-spacing: 0.5px; }
.top-bar button:hover { background: var(--bg3); color: var(--text); border-color: var(--accent2); }
.top-bar button.active { background: var(--gold); color: var(--bg); border-color: var(--gold); font-weight: 600; }
.top-bar .badge { font-size: 0.65em; background: var(--accent); color: white; border-radius: 10px; padding: 1px 6px; margin-left: 4px; vertical-align: middle; }

.story-area { flex: 1; overflow-y: auto; padding: 24px 48px; max-width: 900px; margin: 0 auto; width: 100%; }
.story-block { margin-bottom: 2px; padding: 14px 20px; border-radius: 6px; line-height: 1.8; white-space: pre-wrap; word-wrap: break-word; position: relative; font-size: 0.98em; }
.story-block.player { background: rgba(124, 77, 255, 0.08); border-left: 3px solid var(--accent2); margin: 20px 0; padding: 16px 20px; border-radius: 2px 8px 8px 2px; }
.story-block.player .block-text { font-style: italic; color: var(--text-dim); }
.story-block.player.say .block-text::before { content: '\201C'; color: var(--accent2); font-size: 1.3em; }
.story-block.player.say .block-text::after { content: '\201D'; color: var(--accent2); font-size: 1.3em; }
.story-block.player.do .block-text::before { content: '> '; color: var(--gold); font-style: normal; font-weight: 600; }
.story-block.player.story .block-text::before { content: '\2731 '; color: var(--gold); font-style: normal; }
.story-block.ai { background: transparent; }
.story-block .action-type { font-size: 0.6em; text-transform: uppercase; color: var(--accent2); letter-spacing: 2px; margin-bottom: 4px; font-weight: 700; }
.story-block .edit-btn { position: absolute; top: 50%; right: -36px; transform: translateY(-50%); background: var(--bg3); border: 1px solid var(--border); color: var(--text-dim); cursor: pointer; font-size: 0.7em; opacity: 0; transition: all 0.2s; padding: 4px 8px; border-radius: 4px; white-space: nowrap; }
.story-block:hover .edit-btn { opacity: 0.7; }
.story-block .edit-btn:hover { opacity: 1; color: var(--gold); border-color: var(--gold); }
.story-summary { margin: 0 0 24px; padding: 16px 20px; border-radius: 8px; background: rgba(224, 180, 74, 0.06); border: 1px dashed rgba(224, 180, 74, 0.3); color: var(--text-dim); font-size: 0.85em; line-height: 1.7; white-space: pre-wrap; }
.story-summary::before { content: 'Earlier in the story...'; display: block; font-size: 0.65em; text-transform: uppercase; letter-spacing: 2px; color: var(--gold); margin-bottom: 8px; font-weight: 700; opacity: 0.8; }
.streaming-cursor { display: inline-block; width: 2px; height: 18px; background: var(--gold); animation: blink 0.9s ease-in-out infinite; vertical-align: text-bottom; margin-left: 2px; border-radius: 1px; }
@keyframes blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }

.input-area { padding: 16px 48px 20px; background: var(--bg2); border-top: 1px solid var(--border); max-width: 900px; margin: 0 auto; width: 100%; }
.action-buttons { display: flex; gap: 6px; margin-bottom: 10px; }
.action-buttons button { padding: 5px 18px; border: 1px solid var(--border); background: var(--input-bg); color: var(--text-dim); border-radius: 20px; cursor: pointer; font-size: 0.8em; transition: all 0.2s; letter-spacing: 0.5px; }
.action-buttons button:hover { color: var(--text); border-color: var(--accent2); }
.action-buttons button.active { background: var(--accent2); border-color: var(--accent2); color: white; font-weight: 600; }
.input-row { display: flex; gap: 8px; }
.input-row textarea { flex: 1; padding: 12px 14px; background: var(--input-bg); color: var(--text); border: 1px solid var(--border); border-radius: 10px; font-family: inherit; font-size: 0.95em; resize: none; min-height: 48px; max-height: 200px; line-height: 1.5; transition: border-color 0.2s; }
.input-row textarea:focus { outline: none; border-color: var(--accent2); box-shadow: 0 0 0 2px rgba(124, 77, 255, 0.15); }
.input-row .send-btn { padding: 12px 22px; background: linear-gradient(135deg, var(--accent) 0%, #b82e4a 100%); color: white; border: none; border-radius: 10px; cursor: pointer; font-size: 0.95em; font-weight: bold; transition: all 0.2s; align-self: flex-end; letter-spacing: 0.5px; }
.input-row .send-btn:hover { filter: brightness(1.15); transform: translateY(-1px); }
.input-row .send-btn:disabled { background: var(--border); cursor: not-allowed; filter: none; transform: none; }
.input-controls { display: flex; gap: 6px; margin-top: 8px; }
.input-controls button { padding: 4px 12px; background: none; border: 1px solid var(--border); color: var(--text-dim); border-radius: 6px; cursor: pointer; font-size: 0.75em; transition: all 0.2s; letter-spacing: 0.3px; }
.input-controls button:hover { color: var(--text); border-color: var(--text-dim); }

.welcome { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; padding: 40px; text-align: center; }
.welcome h2 { font-size: 2.2em; color: var(--gold); margin-bottom: 16px; letter-spacing: 3px; font-variant: small-caps; }

.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(4px); z-index: 100; align-items: center; justify-content: center; }
.modal-overlay.active { display: flex; }
.modal { background: var(--bg2); border: 1px solid var(--border); border-radius: 14px; padding: 28px; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 16px 48px var(--shadow); }
.modal h2 { color: var(--gold); margin-bottom: 18px; font-size: 1.2em; letter-spacing: 1px; }
.modal label { display: block; font-size: 0.85em; color: var(--text-dim); margin-bottom: 4px; margin-top: 14px; letter-spacing: 0.3px; }
.modal input, .modal textarea, .modal select { width: 100%; padding: 10px 12px; background: var(--input-bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; font-family: inherit; font-size: 0.9em; transition: border-color 0.2s; }
.modal input:focus, .modal textarea:focus { outline: none; border-color: var(--accent2); }
.modal textarea { min-height: 80px; resize: vertical; }
.modal .btn-row { display: flex; gap: 8px; margin-top: 22px; justify-content: flex-end; }
.modal .btn-row button { padding: 9px 22px; border-radius: 8px; cursor: pointer; font-size: 0.9em; border: 1px solid var(--border); transition: all 0.2s; }
.modal .btn-row .cancel { background: var(--input-bg); color: var(--text-dim); }
.modal .btn-row .cancel:hover { color: var(--text); }
.modal .btn-row .confirm { background: linear-gradient(135deg, var(--accent) 0%, #b82e4a 100%); color: white; border-color: transparent; }
.modal .btn-row .confirm:hover { filter: brightness(1.15); }

.panel { display: none; width: 380px; background: var(--bg2); border-left: 1px solid var(--border); flex-direction: column; flex-shrink: 0; }
.panel.active { display: flex; }
.panel-header { padding: 14px 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
.panel-header h3 { color: var(--gold); font-size: 0.95em; letter-spacing: 1px; font-weight: 600; }
.panel-header button { background: none; border: none; color: var(--text-dim); cursor: pointer; font-size: 1.2em; }
.panel-tabs { display: flex; border-bottom: 1px solid var(--border); background: rgba(0,0,0,0.15); }
.panel-tabs button { flex: 1; padding: 10px; background: none; border: none; border-bottom: 2px solid transparent; color: var(--text-dim); cursor: pointer; font-size: 0.8em; transition: all 0.2s; letter-spacing: 0.5px; text-transform: uppercase; }
.panel-tabs button.active { color: var(--gold); border-bottom-color: var(--gold); }
.panel-tabs button:hover { color: var(--text); }
.panel-body { flex: 1; overflow-y: auto; padding: 14px; }

/* Story Cards */
.wi-card { background: var(--input-bg); border: 1px solid var(--border); border-radius: 10px; padding: 14px; margin-bottom: 8px; cursor: pointer; transition: all 0.2s; }
.wi-card:hover { border-color: var(--gold); box-shadow: 0 2px 12px rgba(224, 180, 74, 0.08); }
.wi-card .wi-header { display: flex; justify-content: space-between; align-items: center; }
.wi-card .wi-title { font-weight: bold; color: var(--gold); font-size: 0.88em; }
.wi-card .wi-keys { font-size: 0.72em; color: var(--text-dim); margin: 4px 0; opacity: 0.8; }
.wi-card .wi-entry { font-size: 0.82em; line-height: 1.6; max-height: 80px; overflow: hidden; white-space: pre-wrap; color: var(--text-dim); }
.wi-card.expanded .wi-entry { max-height: none; }
.wi-card .wi-actions { margin-top: 10px; display: flex; gap: 6px; }
.wi-card .wi-actions button { font-size: 0.72em; padding: 4px 12px; background: none; border: 1px solid var(--border); color: var(--text-dim); border-radius: 6px; cursor: pointer; transition: all 0.2s; }
.wi-card .wi-actions button:hover { color: var(--accent); border-color: var(--accent); }
.wi-card .wi-actions button.edit-card:hover { color: var(--gold); border-color: var(--gold); }

/* Settings */
.setting-group { margin-bottom: 18px; }
.setting-group h4 { color: var(--gold); font-size: 0.8em; margin-bottom: 10px; border-bottom: 1px solid var(--border); padding-bottom: 6px; letter-spacing: 1px; text-transform: uppercase; }
.setting-group label { display: block; font-size: 0.82em; color: var(--text-dim); margin-bottom: 4px; margin-top: 10px; }
.setting-group input[type="text"], .setting-group textarea, .setting-group select { width: 100%; padding: 8px 10px; background: var(--input-bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font-family: inherit; font-size: 0.88em; transition: border-color 0.2s; }
.setting-group input[type="text"]:focus, .setting-group textarea:focus { outline: none; border-color: var(--accent2); }
.setting-group textarea { min-height: 60px; resize: vertical; }

.range-row { display: flex; align-items: center; gap: 10px; margin-top: 4px; }
.range-row input[type="range"] { flex: 1; accent-color: var(--gold); height: 4px; }
.range-row .range-val { font-size: 0.82em; color: var(--gold); min-width: 42px; text-align: right; font-variant-numeric: tabular-nums; }

.toggle-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid rgba(46, 42, 69, 0.5); }
.toggle-row:last-child { border-bottom: none; }
.toggle-row span { font-size: 0.82em; }
.toggle { position: relative; width: 38px; height: 20px; cursor: pointer; }
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle .slider { position: absolute; inset: 0; background: var(--border); border-radius: 10px; transition: background 0.2s; }
.toggle .slider::before { content: ''; position: absolute; width: 14px; height: 14px; left: 3px; top: 3px; background: var(--text); border-radius: 50%; transition: transform 0.2s; }
.toggle input:checked + .slider { background: var(--green); }
.toggle input:checked + .slider::before { transform: translateX(18px); }

.auto-cards-status { font-size: 0.78em; padding: 8px 12px; background: var(--input-bg); border-radius: 6px; margin-top: 10px; color: var(--text-dim); border: 1px solid var(--border); }

.toast { position: fixed; bottom: 24px; right: 24px; background: var(--bg3); color: var(--text); padding: 12px 20px; border-radius: 10px; border: 1px solid var(--gold); z-index: 200; font-size: 0.85em; opacity: 0; transition: opacity 0.3s; pointer-events: none; box-shadow: 0 8px 24px var(--shadow); }
.toast.visible { opacity: 1; }

::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }

@media (max-width: 768px) {
  .sidebar { display: none; }
  .story-area, .input-area { padding-left: 16px; padding-right: 16px; }
  .story-block .edit-btn { right: 4px; top: 4px; transform: none; }
}
</style>
</head>
<body>
<div class="app">
  <div class="sidebar">
    <div class="sidebar-header"><h1>AICastle</h1></div>
    <div class="sidebar-actions">
      <button class="primary" onclick="showModal('newGameModal')">New Adventure</button>
      <button onclick="showModal('importModal')">Import from AI Dungeon</button>
    </div>
    <div class="game-list" id="gameList"></div>
  </div>

  <div class="main">
    <div class="top-bar" id="topBar" style="display:none;">
      <span class="game-title" id="gameTitle">Untitled</span>
      <select class="model-select" id="modelSelect" onchange="switchModel(this.value)"></select>
      <button id="btnWorld" onclick="togglePanel('world')">World</button>
      <button id="btnSettings" onclick="togglePanel('settings')">Settings</button>
    </div>

    <div id="welcomeScreen" class="welcome"><h2>AICastle</h2></div>
    <div id="storyArea" class="story-area" style="display:none;"></div>

    <div id="inputArea" class="input-area" style="display:none;">
      <div class="action-buttons">
        <button class="active" data-type="do" onclick="setActionType('do')">Do</button>
        <button data-type="say" onclick="setActionType('say')">Say</button>
        <button data-type="story" onclick="setActionType('story')">Story</button>
      </div>
      <div class="input-row">
        <textarea id="actionInput" placeholder="What do you do?" rows="1" onkeydown="handleInputKey(event)"></textarea>
        <button class="send-btn" id="sendBtn" onclick="sendAction()">Send</button>
      </div>
      <div class="input-controls">
        <button onclick="sendContinue()">Continue</button>
        <button onclick="retryAction()">Retry</button>
        <button onclick="undoAction()">Undo</button>
      </div>
    </div>
  </div>

  <!-- Right Panel -->
  <div class="panel" id="rightPanel">
    <div class="panel-tabs" id="panelTabs"></div>
    <div class="panel-header">
      <h3 id="panelTitle">Settings</h3>
    </div>

    <!-- Model Tab -->
    <div class="panel-body panel-tab-content" id="tabModel">
      <div class="setting-group">
        <h4>Generation</h4>
        <label title="Controls randomness. Higher = more creative and unpredictable, lower = more focused and deterministic.">Temperature</label>
        <div class="range-row">
          <input type="range" id="msTemp" min="0" max="2" step="0.05" oninput="updateRange(this)" onchange="saveModelSettings()">
          <span class="range-val" id="msTempVal"></span>
        </div>
        <label title="Nucleus sampling. Only considers tokens whose cumulative probability is within this threshold. Lower = less variety.">Top P</label>
        <div class="range-row">
          <input type="range" id="msTopP" min="0" max="1" step="0.05" oninput="updateRange(this)" onchange="saveModelSettings()">
          <span class="range-val" id="msTopPVal"></span>
        </div>
        <label title="Limits the model to the top K most likely tokens at each step. Lower = more predictable.">Top K</label>
        <div class="range-row">
          <input type="range" id="msTopK" min="1" max="100" step="1" oninput="updateRange(this)" onchange="saveModelSettings()">
          <span class="range-val" id="msTopKVal"></span>
        </div>
        <label title="Penalizes repeated words and phrases. Higher = less repetition but can sound unnatural.">Repeat Penalty</label>
        <div class="range-row">
          <input type="range" id="msRepeat" min="1" max="2" step="0.05" oninput="updateRange(this)" onchange="saveModelSettings()">
          <span class="range-val" id="msRepeatVal"></span>
        </div>
        <label title="Maximum number of tokens the model can generate per response. Higher = longer responses but slower.">Max Tokens</label>
        <div class="range-row">
          <input type="range" id="msMaxTok" min="128" max="4096" step="64" oninput="updateRange(this)" onchange="saveModelSettings()">
          <span class="range-val" id="msMaxTokVal"></span>
        </div>
        <label title="Total token budget for the prompt + response. Must fit your model's limit. Higher uses more VRAM and is slower.">Context Window</label>
        <div class="range-row">
          <input type="range" id="msCtxWin" min="2048" max="32768" step="1024" oninput="updateRange(this)" onchange="saveModelSettings()">
          <span class="range-val" id="msCtxWinVal"></span>
        </div>
        <label title="How many recent story actions are sent to the model. Cards and memory always provide long-term context. Lower = faster.">History Actions</label>
        <div class="range-row">
          <input type="range" id="msHistory" min="10" max="200" step="5" oninput="updateRange(this)" onchange="saveModelSettings()">
          <span class="range-val" id="msHistoryVal"></span>
        </div>
      </div>
    </div>

    <!-- Plot Tab -->
    <div class="panel-body panel-tab-content" id="tabPlot" style="display:none;">
      <div class="setting-group">
        <h4>Adventure</h4>
        <label>Title</label>
        <input type="text" id="settingsTitle" onchange="saveSettings()">
      </div>
      <div class="setting-group">
        <h4>Memory</h4>
        <textarea id="settingsMemory" rows="4" placeholder="Persistent context the AI always sees..." onchange="saveSettings()"></textarea>
      </div>
      <div class="setting-group">
        <h4>Author's Note</h4>
        <textarea id="settingsAuthorsNote" rows="3" placeholder="Style and tone guidance..." onchange="saveSettings()"></textarea>
      </div>
      <div class="setting-group" style="margin-top:16px;">
        <button style="width:100%;padding:9px;background:var(--input-bg);color:var(--text);border:1px solid var(--border);border-radius:8px;cursor:pointer;font-size:0.85em;transition:all 0.2s;" onmouseover="this.style.borderColor='var(--gold)'" onmouseout="this.style.borderColor='var(--border)'" onclick="exportCurrentGame()">Export Adventure</button>
      </div>
      <div class="setting-group" style="margin-top:8px;">
        <button style="width:100%;padding:9px;background:rgba(214,59,90,0.15);color:var(--accent);border:1px solid rgba(214,59,90,0.3);border-radius:8px;cursor:pointer;font-size:0.85em;transition:all 0.2s;" onmouseover="this.style.background='var(--accent)';this.style.color='white'" onmouseout="this.style.background='rgba(214,59,90,0.15)';this.style.color='var(--accent)'" onclick="deleteCurrentGame()">Delete Adventure</button>
      </div>
    </div>

    <!-- Cards Tab -->
    <div class="panel-body panel-tab-content" id="tabCards" style="display:none;">
      <div class="setting-group">
        <h4 style="display:flex;justify-content:space-between;align-items:center;">
          Story Cards <span class="badge" id="cardCount">0</span>
          <span style="font-size:0.8em;font-weight:normal;color:var(--text-dim);" id="cardCountLabel">0 cards</span>
        </h4>
        <div style="display:flex;gap:8px;margin-bottom:8px;">
          <button style="flex:1;padding:7px;background:var(--input-bg);border:1px solid var(--border);color:var(--text);border-radius:8px;cursor:pointer;font-size:0.82em;transition:all 0.2s;" onmouseover="this.style.borderColor='var(--gold)'" onmouseout="this.style.borderColor='var(--border)'" onclick="showModal('addWIModal')">+ Add</button>
          <button style="flex:1;padding:7px;background:rgba(124,77,255,0.15);border:1px solid rgba(124,77,255,0.3);color:var(--accent2);border-radius:8px;cursor:pointer;font-size:0.82em;transition:all 0.2s;" onmouseover="this.style.background='var(--accent2)';this.style.color='white'" onmouseout="this.style.background='rgba(124,77,255,0.15)';this.style.color='var(--accent2)'" onclick="runAutoCardsDetect()">Auto-Detect</button>
        </div>
        <input type="text" id="cardSearch" placeholder="Search cards..." style="width:100%;padding:6px 8px;background:var(--input-bg);color:var(--text);border:1px solid var(--border);border-radius:4px;font-size:0.85em;margin-bottom:8px;" oninput="filterCards()">
        <div id="worldInfoList"></div>
      </div>
    </div>

    <!-- Auto-Cards Tab -->
    <div class="panel-body panel-tab-content" id="tabAutocards" style="display:none;">
      <div class="setting-group">
        <h4>Auto-Cards</h4>
        <div class="toggle-row"><span>Enabled</span><label class="toggle"><input type="checkbox" id="acEnabled" onchange="saveAutoCards()"><span class="slider"></span></label></div>
        <div class="toggle-row"><span>Memory updates</span><label class="toggle"><input type="checkbox" id="acMemoryUpdates" onchange="saveAutoCards()"><span class="slider"></span></label></div>
        <div class="toggle-row"><span>Bulleted format</span><label class="toggle"><input type="checkbox" id="acBulleted" onchange="saveAutoCards()"><span class="slider"></span></label></div>
        <div class="toggle-row"><span>Exclude ALL-CAPS</span><label class="toggle"><input type="checkbox" id="acExcludeAllcaps" onchange="saveAutoCards()"><span class="slider"></span></label></div>
        <div class="toggle-row"><span>Detect from player</span><label class="toggle"><input type="checkbox" id="acDetectPlayer" onchange="saveAutoCards()"><span class="slider"></span></label></div>
      </div>
      <div class="setting-group">
        <label>Cooldown turns</label>
        <div class="range-row">
          <input type="range" id="acCooldown" min="1" max="50" step="1" oninput="updateRange(this)" onchange="saveAutoCards()">
          <span class="range-val" id="acCooldownVal"></span>
        </div>
        <label>Max entry length</label>
        <div class="range-row">
          <input type="range" id="acMaxLength" min="100" max="2000" step="50" oninput="updateRange(this)" onchange="saveAutoCards()">
          <span class="range-val" id="acMaxLengthVal"></span>
        </div>
        <label>Memory bank length</label>
        <div class="range-row">
          <input type="range" id="acMemoryLength" min="500" max="5000" step="250" oninput="updateRange(this)" onchange="saveAutoCards()">
          <span class="range-val" id="acMemoryLengthVal"></span>
        </div>
        <label>Min turns for detection</label>
        <div class="range-row">
          <input type="range" id="acMinTurns" min="1" max="20" step="1" oninput="updateRange(this)" onchange="saveAutoCards()">
          <span class="range-val" id="acMinTurnsVal"></span>
        </div>
      </div>
      <div class="auto-cards-status" id="acStatus"></div>
    </div>
  </div>
</div>

<div class="toast" id="toast"></div>

<!-- New Game Modal -->
<div class="modal-overlay" id="newGameModal">
  <div class="modal">
    <h2>New Adventure</h2>
    <label>Model</label><select id="newGameModel"></select>
    <label>Title</label><input id="newGameTitle" placeholder="My Adventure">
    <label>Starting Prompt</label><textarea id="newGamePrompt" placeholder="You are a knight in a dark fantasy world..."></textarea>
    <label>Memory (optional)</label><textarea id="newGameMemory" rows="3" placeholder="Persistent context..."></textarea>
    <label>Author's Note (optional)</label><textarea id="newGameAuthorsNote" rows="2" placeholder="Style guidance..."></textarea>
    <div class="btn-row"><button class="cancel" onclick="closeModal('newGameModal')">Cancel</button><button class="confirm" onclick="createGame()">Create</button></div>
  </div>
</div>

<!-- Import Modal -->
<div class="modal-overlay" id="importModal">
  <div class="modal">
    <h2>Import from AI Dungeon</h2>
    <label>Files (adventure text + story cards)</label>
    <input type="file" id="importFile" accept=".json,.txt,.md,.zip" multiple style="margin-top:4px;">
    <div class="btn-row"><button class="cancel" onclick="closeModal('importModal')">Cancel</button><button class="confirm" onclick="importFile()">Import</button></div>
  </div>
</div>

<!-- Add Card Modal -->
<div class="modal-overlay" id="addWIModal">
  <div class="modal">
    <h2>Add Story Card</h2>
    <label>Title</label><input id="wiTitle" placeholder="Character, location...">
    <label>Keys (comma-separated triggers)</label><input id="wiKeys" placeholder="dragon, Aldric">
    <label>Entry</label><textarea id="wiEntry" rows="4" placeholder="Information..."></textarea>
    <div class="btn-row"><button class="cancel" onclick="closeModal('addWIModal')">Cancel</button><button class="confirm" onclick="addWorldInfo()">Add</button></div>
  </div>
</div>

<!-- Edit Card Modal -->
<div class="modal-overlay" id="editCardModal">
  <div class="modal">
    <h2>Edit Story Card</h2>
    <label>Title</label><input id="ecTitle">
    <label>Keys</label><input id="ecKeys">
    <label>Entry</label><textarea id="ecEntry" rows="6"></textarea>
    <input type="hidden" id="ecIndex">
    <div class="btn-row"><button class="cancel" onclick="closeModal('editCardModal')">Cancel</button><button class="confirm" onclick="saveCardEdit()">Save</button></div>
  </div>
</div>

<!-- Edit Action Modal -->
<div class="modal-overlay" id="editModal">
  <div class="modal">
    <h2>Edit</h2>
    <textarea id="editText" rows="8"></textarea>
    <input type="hidden" id="editIndex">
    <div class="btn-row"><button class="cancel" onclick="closeModal('editModal')">Cancel</button><button class="confirm" onclick="saveEdit()">Save</button></div>
  </div>
</div>

<!-- Auto-Detect Modal -->
<div class="modal-overlay" id="reviewCardModal">
  <div class="modal">
    <h2>Review Card <span id="rcProgress" style="font-size:0.7em;color:var(--text-dim);"></span></h2>
    <label>Title</label>
    <input type="text" id="rcTitle" style="width:100%;margin-bottom:8px;">
    <label>Keys</label>
    <input type="text" id="rcKeys" style="width:100%;margin-bottom:8px;">
    <label>Entry</label>
    <textarea id="rcEntry" rows="8" style="width:100%;"></textarea>
    <div class="btn-row" style="margin-top:12px;">
      <button class="cancel" onclick="reviewReject()">Reject</button>
      <button style="background:var(--input-bg);color:var(--text-dim);border:1px solid var(--border);border-radius:8px;padding:8px 18px;cursor:pointer;font-size:0.9em;transition:all 0.2s;" onmouseover="this.style.color='var(--text)';this.style.borderColor='var(--text-dim)'" onmouseout="this.style.color='var(--text-dim)';this.style.borderColor='var(--border)'" onclick="reviewRegen()">Regenerate</button>
      <button class="confirm" onclick="reviewAccept()">Accept</button>
    </div>
  </div>
</div>

<script>
// ---------------------------------------------------------------------------
// Global state
// ---------------------------------------------------------------------------
let currentGame = null;      // full GameState of the open adventure
let actionType = 'do';       // selected input mode: do | say | story
let streaming = false;       // true while an AI response is being generated
let models = [];             // Ollama models for the dropdowns
let activeTab = 'settings';

// The right panel has two groups (World / Settings), each with its own tabs.
const PANEL_GROUPS = {
  world: {
    tabs: {
      plot: { label: 'Plot', title: 'Plot' },
      cards: { label: 'Cards', title: 'Story Cards' },
    },
    default: 'plot',
  },
  settings: {
    tabs: {
      model: { label: 'Model', title: 'Model Tuning' },
      autocards: { label: 'Auto-Cards', title: 'Auto-Cards' },
    },
    default: 'model',
  },
};

// ---------------------------------------------------------------------------
// Small helpers
// ---------------------------------------------------------------------------

// Mirror a range slider's value into its "<id>Val" label.
function updateRange(el) {
  const valEl = document.getElementById(el.id + 'Val');
  if (valEl) valEl.textContent = el.value;
}

// Normalize a comma-separated trigger-key list: trim, strip punctuation,
// dedupe case-insensitively, capitalize.
function cleanKeys(raw) {
  if (!raw) return '';
  const parts = raw.split(',').map(k => k.trim()).filter(k => k.length > 0);
  const seen = new Set();
  const clean = [];
  for (const p of parts) {
    const base = p.replace(/['".,?!;:()\[\]—]/g, '').trim().toLowerCase();
    if (base && !seen.has(base)) { seen.add(base); clean.push(base.charAt(0).toUpperCase() + base.slice(1)); }
  }
  return clean.join(', ');
}

// Strip leftover {title: ...} braces from imported card entries.
function cleanEntry(text) {
  if (!text) return '';
  return text.replace(/^\{[^}]*\}\s*[-–—\n]*/i, '').replace(/\{[^}]*\}\s*$/,'').trim();
}

// ---------------------------------------------------------------------------
// Data loading & rendering
// ---------------------------------------------------------------------------

async function init() { await loadModels(); await loadGames(); }

async function loadModels() {
  try { const r = await fetch('/api/models'); const d = await r.json(); models = d.models; populateModelSelects(); } catch(e) {}
}

function populateModelSelects() {
  ['modelSelect','newGameModel'].forEach(id => {
    const el = document.getElementById(id);
    if (el) el.innerHTML = models.map(m => `<option value="${m.name}">${m.name}</option>`).join('');
  });
}

async function loadGames() {
  const r = await fetch('/api/games'); const d = await r.json();
  document.getElementById('gameList').innerHTML = d.games.map(g => `
    <div class="game-item ${currentGame?.id===g.id?'active':''}" onclick="loadGame('${g.id}')">
      <div class="title">${esc(g.title)}</div>
      <div class="meta">${g.action_count} actions &middot; ${g.model}</div>
    </div>`).join('');
}

async function loadGame(id) {
  const r = await fetch(`/api/game/${id}`); const d = await r.json();
  currentGame = d.game;
  if (!currentGame.auto_cards) currentGame.auto_cards = {};
  if (!currentGame.model_settings) currentGame.model_settings = {};

  document.getElementById('welcomeScreen').style.display = 'none';
  document.getElementById('topBar').style.display = 'flex';
  document.getElementById('storyArea').style.display = 'block';
  document.getElementById('inputArea').style.display = 'block';
  document.getElementById('gameTitle').textContent = currentGame.title;
  document.getElementById('cardCount').textContent = currentGame.world_info.length;

  if (models.length) document.getElementById('modelSelect').value = currentGame.model;
  document.getElementById('settingsTitle').value = currentGame.title;
  document.getElementById('settingsMemory').value = currentGame.memory;
  document.getElementById('settingsAuthorsNote').value = currentGame.authors_note;

  loadModelSettingsUI();
  loadAutoCardsUI();
  renderStory();
  renderWorldInfo();
  loadGames();
  setTimeout(() => { const a = document.getElementById('storyArea'); a.scrollTop = a.scrollHeight; }, 50);
}

function loadModelSettingsUI() {
  const ms = currentGame.model_settings;
  setRange('msTemp', ms.temperature ?? 0.8);
  setRange('msTopP', ms.top_p ?? 0.9);
  setRange('msTopK', ms.top_k ?? 40);
  setRange('msRepeat', ms.repeat_penalty ?? 1.1);
  setRange('msMaxTok', ms.max_tokens ?? 1024);
  setRange('msCtxWin', ms.context_window ?? 8192);
  setRange('msHistory', ms.history_actions ?? 40);
}

function setRange(id, val) {
  const el = document.getElementById(id);
  el.value = val;
  document.getElementById(id + 'Val').textContent = val;
}

function loadAutoCardsUI() {
  const ac = currentGame.auto_cards;
  document.getElementById('acEnabled').checked = ac.enabled !== false;
  document.getElementById('acMemoryUpdates').checked = ac.memory_updates !== false;
  document.getElementById('acBulleted').checked = ac.bulleted_format !== false;
  document.getElementById('acExcludeAllcaps').checked = ac.exclude_allcaps !== false;
  document.getElementById('acDetectPlayer').checked = ac.detect_from_player === true;
  setRange('acCooldown', ac.cooldown_turns || 22);
  setRange('acMaxLength', ac.max_entry_length || 750);
  setRange('acMemoryLength', ac.memory_bank_length || 2750);
  setRange('acMinTurns', ac.min_turns_for_detection || 5);
  document.getElementById('acStatus').textContent = `Turns since last card: ${ac.turns_since_last_card||0} / ${ac.cooldown_turns||22}`;
}

function renderStory() {
  const area = document.getElementById('storyArea');
  if (!currentGame) return;
  let html = '';
  if (currentGame.story_summary) {
    html += `<div class="story-summary">${esc(currentGame.story_summary)}</div>`;
  }
  html += currentGame.actions.map((a,i) => {
    const isP = a.type !== 'continue';
    const lbl = isP ? a.type.toUpperCase() : '';
    return `<div class="story-block ${isP ? 'player ' + a.type : 'ai'}">
      ${lbl?`<div class="action-type">${lbl}</div>`:''}
      <span class="block-text">${esc(a.text)}</span>
      <button class="edit-btn" onclick="showEditModal(${i})">edit</button>
    </div>`;
  }).join('');
  area.innerHTML = html;
}

function renderWorldInfo() {
  const list = document.getElementById('worldInfoList');
  if (!currentGame) return;
  const count = currentGame.world_info.length;
  document.getElementById('cardCount').textContent = count;
  document.getElementById('cardCountLabel').textContent = count + ' card' + (count!==1?'s':'');

  list.innerHTML = currentGame.world_info.map((wi,i) => `
    <div class="wi-card" id="wiCard${i}" onclick="toggleCardExpand(${i})">
      <div class="wi-header">
        <div class="wi-title">${esc(wi.title || 'Untitled')}</div>
      </div>
      <div class="wi-keys">${esc(cleanKeys(wi.keys))}</div>
      <div class="wi-entry">${esc(cleanEntry(wi.entry))}</div>
      <div class="wi-actions" onclick="event.stopPropagation()">
        <button class="edit-card" onclick="showEditCardModal(${i})">Edit</button>
        <button onclick="removeWorldInfo(${i})">Remove</button>
      </div>
    </div>`).join('');
}

function toggleCardExpand(i) { document.getElementById('wiCard'+i).classList.toggle('expanded'); }

function filterCards() {
  const q = document.getElementById('cardSearch').value.toLowerCase();
  document.querySelectorAll('.wi-card').forEach((card,i) => {
    const wi = currentGame.world_info[i];
    card.style.display = (!q || (wi.title||'').toLowerCase().includes(q) || (wi.entry||'').toLowerCase().includes(q)) ? '' : 'none';
  });
}

// ---------------------------------------------------------------------------
// Right panel & tabs
// ---------------------------------------------------------------------------

let activePanel = null;

const ALL_TAB_IDS = ['tabPlot', 'tabCards', 'tabModel', 'tabAutocards'];

function buildPanelTabs(panelName, activeTabKey) {
  const group = PANEL_GROUPS[panelName];
  if (!group) return;
  const container = document.getElementById('panelTabs');
  container.innerHTML = Object.entries(group.tabs).map(([k,v]) =>
    `<button data-tab="${k}" class="${k===activeTabKey?'active':''}" onclick="switchTab('${k}')">${v.label}</button>`
  ).join('');
}

function tabIdFromKey(key) {
  const map = { plot:'tabPlot', cards:'tabCards', model:'tabModel', autocards:'tabAutocards' };
  return map[key];
}

function panelForTab(tabKey) {
  for (const [pn, pg] of Object.entries(PANEL_GROUPS)) {
    if (tabKey in pg.tabs) return pn;
  }
  return null;
}

function switchTab(tab) {
  activeTab = tab;
  const pn = panelForTab(tab);
  if (pn) {
    activePanel = pn;
    buildPanelTabs(pn, tab);
    document.getElementById('panelTitle').textContent = PANEL_GROUPS[pn].tabs[tab]?.title || '';
  }
  ALL_TAB_IDS.forEach(id => { document.getElementById(id).style.display = 'none'; });
  const el = document.getElementById(tabIdFromKey(tab));
  if (el) el.style.display = '';
  updateTopBarHighlight();
}

function updateTopBarHighlight() {
  const btnWorld = document.getElementById('btnWorld');
  const btnSettings = document.getElementById('btnSettings');
  const panel = document.getElementById('rightPanel');
  const open = panel.classList.contains('active');
  btnWorld?.classList.toggle('active', open && activePanel === 'world');
  btnSettings?.classList.toggle('active', open && activePanel === 'settings');
}

function togglePanel(name) {
  const panel = document.getElementById('rightPanel');
  if (name === null) { panel.classList.remove('active'); updateTopBarHighlight(); return; }
  const wasActive = panel.classList.contains('active') && activePanel === name;
  if (wasActive) { panel.classList.remove('active'); updateTopBarHighlight(); return; }

  const group = PANEL_GROUPS[name];
  const defaultTab = group ? group.default : name;
  switchTab(defaultTab);
  panel.classList.add('active');
  updateTopBarHighlight();
}

function setActionType(type) {
  actionType = type;
  document.querySelectorAll('.action-buttons button').forEach(b => b.classList.toggle('active', b.dataset.type === type));
  const input = document.getElementById('actionInput');
  input.placeholder = {do:'What do you do?', say:'What do you say?', story:'Describe what happens...'}[type]||'';
  input.focus();
}

function handleInputKey(e) { if (e.key==='Enter' && !e.shiftKey) { e.preventDefault(); sendAction(); } }

// ---------------------------------------------------------------------------
// Story actions & SSE streaming
// ---------------------------------------------------------------------------

// Lock/unlock the input controls while a response is generating.
function setStreaming(on) {
  streaming = on;
  document.getElementById('sendBtn').disabled = on;
  document.querySelectorAll('.input-controls button').forEach(b => b.style.opacity = on ? '0.4' : '');
}

async function sendAction() {
  if (streaming || !currentGame) return;
  const input = document.getElementById('actionInput');
  const text = input.value.trim();
  if (!text) return;
  input.value = '';
  setStreaming(true);
  currentGame.actions.push({type:actionType, text, timestamp:Date.now()/1000});
  renderStory(); scrollToBottom();
  await streamResponse('/api/action', {game_id:currentGame.id, action_type:actionType, text, model:document.getElementById('modelSelect').value});
  triggerAutoCards();
}

async function sendContinue() {
  if (streaming || !currentGame) return;
  setStreaming(true);
  await streamResponse('/api/action', {game_id:currentGame.id, action_type:'continue', text:'', model:document.getElementById('modelSelect').value});
  triggerAutoCards();
}

async function retryAction() {
  if (streaming || !currentGame) return;
  setStreaming(true);
  if (currentGame.actions.length && currentGame.actions[currentGame.actions.length-1].type==='continue') { currentGame.actions.pop(); renderStory(); }
  await streamResponse('/api/action/retry', {game_id:currentGame.id, action_type:'continue', text:'', model:document.getElementById('modelSelect').value});
}

// POST to an SSE endpoint and stream the AI text into a new story block.
// Events: {chunk} appends text, {error} shows a message, {done} commits the
// action to local state. An empty failed block is removed entirely.
async function streamResponse(url, body) {
  const area = document.getElementById('storyArea');
  const block = document.createElement('div');
  block.className = 'story-block ai';
  block.innerHTML = '<span class="block-text"></span><span class="streaming-cursor"></span>';
  area.appendChild(block); scrollToBottom();
  const textEl = block.querySelector('.block-text');
  let gotContent = false;
  try {
    const resp = await fetch(url, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)});
    if (!resp.ok) {
      textEl.textContent = `[Server error: ${resp.status} ${resp.statusText}]`;
    } else {
      const reader = resp.body.getReader(); const decoder = new TextDecoder();
      let fullText='', buffer='';
      while (true) {
        const {done, value} = await reader.read(); if (done) break;
        buffer += decoder.decode(value, {stream:true});
        const lines = buffer.split('\n'); buffer = lines.pop();
        for (const line of lines) {
          if (!line.startsWith('data: ')) continue;
          try {
            const data = JSON.parse(line.slice(6));
            if (data.retry) { textEl.textContent = ''; fullText = ''; gotContent = false; showToast(data.retry); }
            if (data.chunk) { fullText += data.chunk; textEl.textContent = fullText; scrollToBottom(); gotContent = true; }
            if (data.error) { textEl.textContent = `[${data.error}]`; gotContent = true; }
            if (data.done) currentGame.actions.push({type:'continue', text:fullText, timestamp:Date.now()/1000});
          } catch(e){}
        }
      }
    }
  } catch(e) { textEl.textContent += `\n[Connection error: ${e.message}]`; }
  block.querySelector('.streaming-cursor')?.remove();
  if (!gotContent && !textEl.textContent) { block.remove(); }
  else {
    const editBtn = document.createElement('button');
    editBtn.className = 'edit-btn'; editBtn.textContent = 'edit';
    editBtn.onclick = () => showEditModal(currentGame.actions.length-1);
    block.appendChild(editBtn);
  }
  setStreaming(false);
}

async function triggerAutoCards() {
  if (!currentGame?.auto_cards?.enabled) return;
  try {
    const r = await fetch('/api/auto-cards/detect', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, action_type:'', text:'', model:currentGame.model})});
    const d = await r.json();
    if (d.titles?.length > 0) {
      await generateAllPreviews(d.titles);
    }
  } catch(e){}
}

async function runAutoCardsDetect() {
  if (!currentGame) return;
  showToast('Scanning...');
  try {
    const r = await fetch('/api/auto-cards/detect', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, action_type:'', text:'', model:currentGame.model})});
    const d = await r.json();
    if (d.skipped) { showToast('Skipped: '+d.skipped); return; }
    if (!d.titles?.length) { showToast('No new titles'); return; }
    await generateAllPreviews(d.titles);
  } catch(e) { showToast('Failed: '+e.message); }
}

// ---------------------------------------------------------------------------
// Auto-cards review flow: detect titles → generate all previews in parallel
// → present them one at a time for Accept / Regenerate / Reject
// ---------------------------------------------------------------------------

let pendingCards = [];      // previews waiting for review
let pendingCardIndex = 0;   // position in pendingCards

async function generateAllPreviews(titles) {
  showToast(`Generating ${titles.length} card(s) in parallel...`);
  const r = await fetch('/api/auto-cards/preview-batch', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, titles, model:currentGame.model})});
  const d = await r.json();
  pendingCards = d.cards;
  pendingCardIndex = 0;
  showNextReview();
}

function showNextReview() {
  if (pendingCardIndex >= pendingCards.length) {
    closeModal('reviewCardModal');
    showToast('All cards reviewed');
    return;
  }
  const card = pendingCards[pendingCardIndex];
  document.getElementById('rcTitle').value = card.title;
  document.getElementById('rcKeys').value = card.keys;
  document.getElementById('rcEntry').value = card.entry;
  const label = card.is_update ? 'Update' : 'New';
  document.getElementById('rcProgress').textContent = `${pendingCardIndex+1} / ${pendingCards.length} (${label})`;
  showModal('reviewCardModal');
}

async function reviewAccept() {
  const title = document.getElementById('rcTitle').value;
  const keys = document.getElementById('rcKeys').value;
  const entry = document.getElementById('rcEntry').value;
  const card = pendingCards[pendingCardIndex];
  if (card.is_update && card.existing_index >= 0) {
    await fetch('/api/world-info/edit', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, index:card.existing_index, title, keys, entry})});
  } else {
    await fetch('/api/world-info/add', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, title, keys, entry})});
  }
  await loadGame(currentGame.id);
  renderWorldInfo();
  pendingCardIndex++;
  showNextReview();
}

async function reviewReject() {
  pendingCardIndex++;
  showNextReview();
}

async function reviewRegen() {
  const title = document.getElementById('rcTitle').value;
  showToast(`Regenerating card for "${title}"...`);
  const fd = new FormData(); fd.append('game_id',currentGame.id); fd.append('title',title); fd.append('model',currentGame.model);
  const r = await fetch('/api/auto-cards/preview', {method:'POST', body:fd});
  const card = await r.json();
  pendingCards[pendingCardIndex] = card;
  document.getElementById('rcTitle').value = card.title;
  document.getElementById('rcKeys').value = card.keys;
  document.getElementById('rcEntry').value = card.entry;
}

async function undoAction() {
  if (streaming||!currentGame) return;
  const r = await fetch('/api/action/undo', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, action_type:'', text:''})});
  currentGame.actions = (await r.json()).actions; renderStory();
}

function scrollToBottom() { const a = document.getElementById('storyArea'); a.scrollTop = a.scrollHeight; }

// ---------------------------------------------------------------------------
// Settings persistence (model, plot, sliders, auto-cards)
// ---------------------------------------------------------------------------

async function switchModel(model) {
  if (!currentGame) return;
  await fetch('/api/settings', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, model})});
  currentGame.model = model;
}

async function saveSettings() {
  if (!currentGame) return;
  const title = document.getElementById('settingsTitle').value;
  const memory = document.getElementById('settingsMemory').value;
  const authorsNote = document.getElementById('settingsAuthorsNote').value;
  await fetch('/api/settings', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, title, memory, authors_note:authorsNote})});
  currentGame.title=title; currentGame.memory=memory; currentGame.authors_note=authorsNote;
  document.getElementById('gameTitle').textContent = title; loadGames();
}

async function saveModelSettings() {
  if (!currentGame) return;
  const ms = {
    temperature: parseFloat(document.getElementById('msTemp').value),
    top_p: parseFloat(document.getElementById('msTopP').value),
    top_k: parseInt(document.getElementById('msTopK').value),
    repeat_penalty: parseFloat(document.getElementById('msRepeat').value),
    max_tokens: parseInt(document.getElementById('msMaxTok').value),
    context_window: parseInt(document.getElementById('msCtxWin').value),
    history_actions: parseInt(document.getElementById('msHistory').value),
  };
  await fetch('/api/settings', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, model_settings:ms})});
  currentGame.model_settings = ms;
}

async function saveAutoCards() {
  if (!currentGame) return;
  const ac = {
    enabled: document.getElementById('acEnabled').checked,
    memory_updates: document.getElementById('acMemoryUpdates').checked,
    bulleted_format: document.getElementById('acBulleted').checked,
    exclude_allcaps: document.getElementById('acExcludeAllcaps').checked,
    detect_from_player: document.getElementById('acDetectPlayer').checked,
    cooldown_turns: parseInt(document.getElementById('acCooldown').value),
    max_entry_length: parseInt(document.getElementById('acMaxLength').value),
    memory_bank_length: parseInt(document.getElementById('acMemoryLength').value),
    min_turns_for_detection: parseInt(document.getElementById('acMinTurns').value),
  };
  await fetch('/api/settings', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, auto_cards:ac})});
  currentGame.auto_cards = {...currentGame.auto_cards, ...ac};
}

async function exportCurrentGame() {
  if (!currentGame) return;
  const a = document.createElement('a');
  a.href = `/api/game/${currentGame.id}/export`;
  a.download = '';
  document.body.appendChild(a);
  a.click();
  a.remove();
}

async function deleteCurrentGame() {
  if (!currentGame||!confirm('Delete this adventure?')) return;
  await fetch(`/api/game/${currentGame.id}`, {method:'DELETE'});
  currentGame=null;
  document.getElementById('welcomeScreen').style.display='flex';
  document.getElementById('topBar').style.display='none';
  document.getElementById('storyArea').style.display='none';
  document.getElementById('inputArea').style.display='none';
  togglePanel(null); loadGames();
}

// ---------------------------------------------------------------------------
// Modals, import/export, misc
// ---------------------------------------------------------------------------

function showModal(id) { document.getElementById(id).classList.add('active'); }
function closeModal(id) { document.getElementById(id).classList.remove('active'); }

function showEditModal(index) {
  document.getElementById('editText').value = currentGame.actions[index].text;
  document.getElementById('editIndex').value = index;
  showModal('editModal');
}

function showEditCardModal(index) {
  const wi = currentGame.world_info[index];
  document.getElementById('ecTitle').value = wi.title || '';
  document.getElementById('ecKeys').value = wi.keys || '';
  document.getElementById('ecEntry').value = wi.entry || '';
  document.getElementById('ecIndex').value = index;
  showModal('editCardModal');
}

async function saveCardEdit() {
  const index = parseInt(document.getElementById('ecIndex').value);
  const title = document.getElementById('ecTitle').value;
  const keys = document.getElementById('ecKeys').value;
  const entry = document.getElementById('ecEntry').value;
  await fetch('/api/world-info/edit', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, index, title, keys, entry})});
  currentGame.world_info[index] = {...currentGame.world_info[index], title, keys, entry};
  renderWorldInfo(); closeModal('editCardModal');
}

async function saveEdit() {
  const index = parseInt(document.getElementById('editIndex').value);
  const text = document.getElementById('editText').value;
  await fetch('/api/action/edit', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, action_index:index, new_text:text})});
  currentGame.actions[index].text = text; renderStory(); closeModal('editModal');
}

async function createGame() {
  const model = document.getElementById('newGameModel').value;
  const title = document.getElementById('newGameTitle').value || 'Untitled Adventure';
  const prompt = document.getElementById('newGamePrompt').value;
  const memory = document.getElementById('newGameMemory').value;
  const authorsNote = document.getElementById('newGameAuthorsNote').value;
  closeModal('newGameModal');
  const r = await fetch('/api/games/new', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({model, title, prompt, memory, authors_note:authorsNote})});
  currentGame = (await r.json()).game; await loadGame(currentGame.id);
}

async function importFile() {
  const fi = document.getElementById('importFile');
  if (!fi.files.length) return;
  const fd = new FormData();
  for (const f of fi.files) fd.append('files', f);
  fd.append('model', models.length ? models[0].name : 'gemma3:12b');
  closeModal('importModal');
  const r = await fetch('/api/import/aidungeon', {method:'POST', body:fd});
  const d = await r.json();
  showToast(`Imported: ${d.action_count} actions, ${d.card_count} cards`);
  await loadGame(d.game.id); loadGames();
}

async function addWorldInfo() {
  if (!currentGame) return;
  const title = document.getElementById('wiTitle').value;
  const keys = document.getElementById('wiKeys').value;
  const entry = document.getElementById('wiEntry').value;
  if (!keys||!entry) return;
  await fetch('/api/world-info/add', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({game_id:currentGame.id, title, keys, entry})});
  currentGame.world_info.push({title, keys, entry}); renderWorldInfo(); closeModal('addWIModal');
  document.getElementById('wiTitle').value=''; document.getElementById('wiKeys').value=''; document.getElementById('wiEntry').value='';
}

async function removeWorldInfo(index) {
  if (!currentGame) return;
  const fd = new FormData(); fd.append('game_id',currentGame.id); fd.append('index',index);
  await fetch('/api/world-info/remove', {method:'POST', body:fd});
  currentGame.world_info.splice(index,1); renderWorldInfo();
}

function showToast(msg) {
  const t = document.getElementById('toast'); t.textContent=msg; t.classList.add('visible');
  setTimeout(()=>t.classList.remove('visible'), 3000);
}

function esc(str) { const d=document.createElement('div'); d.textContent=str||''; return d.innerHTML; }

document.addEventListener('input', e => {
  if (e.target.id==='actionInput') { e.target.style.height='auto'; e.target.style.height=Math.min(e.target.scrollHeight,200)+'px'; }
});
document.querySelectorAll('.modal-overlay').forEach(o => o.addEventListener('click', e => { if(e.target===o) o.classList.remove('active'); }));

init();
</script>
</body>
</html>