const activeTabClasses = ["tab-btn-active"]; const inactiveTabClasses = ["tab-btn-inactive"]; const BOT_OPTIONS_PRESETS = { default: { response_mode: "free-will", free_will_chance: 2, memory_chance: 30, mention_probability: 0, gif_search_enabled: false, image_gen_enabled: false, nsfw_image_enabled: false, spontaneous_posts_enabled: true, }, lurker: { response_mode: "mention-only", free_will_chance: 0, memory_chance: 18, mention_probability: 0, gif_search_enabled: false, image_gen_enabled: false, nsfw_image_enabled: false, spontaneous_posts_enabled: false, }, balanced: { response_mode: "free-will", free_will_chance: 6, memory_chance: 45, mention_probability: 8, gif_search_enabled: true, image_gen_enabled: false, nsfw_image_enabled: false, spontaneous_posts_enabled: true, }, chaos: { response_mode: "free-will", free_will_chance: 22, memory_chance: 65, mention_probability: 35, gif_search_enabled: true, image_gen_enabled: true, nsfw_image_enabled: true, spontaneous_posts_enabled: true, }, }; function switchTab(button, tabName) { document.querySelectorAll(".tab-btn").forEach((tab) => { tab.classList.remove(...activeTabClasses); tab.classList.add(...inactiveTabClasses); }); document.querySelectorAll(".tab-panel").forEach((panel) => { panel.classList.add("hidden"); }); button.classList.remove(...inactiveTabClasses); button.classList.add(...activeTabClasses); document.getElementById(`tab-${tabName}`)?.classList.remove("hidden"); } function setActiveGuildById(guildId) { document.querySelectorAll(".guild-list-item").forEach((item) => { const isActive = item.dataset.guildId === guildId; item.classList.toggle("guild-item-active", isActive); item.classList.toggle("guild-item-inactive", !isActive); }); } function setActiveGuildFromPath() { const match = window.location.pathname.match(/\/dashboard\/guild\/([^/]+)/); if (!match) { setActiveGuildById(""); return; } setActiveGuildById(match[1]); } document.addEventListener("click", (event) => { const item = event.target.closest(".guild-list-item"); if (!item) { return; } setActiveGuildById(item.dataset.guildId); }); document.addEventListener("htmx:afterSwap", (event) => { if (event.target && event.target.id === "guild-main-content") { setActiveGuildFromPath(); initBotOptionsUI(event.target); } }); setActiveGuildFromPath(); initBotOptionsUI(document); function showNotification(message, type) { const existing = document.querySelector(".notification"); if (existing) { existing.remove(); } const notification = document.createElement("div"); notification.className = `notification fixed bottom-5 right-5 z-[200] rounded-lg px-5 py-3 text-sm font-medium text-white shadow-lg ${type === "success" ? "bg-emerald-500" : "bg-rose-500"}`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.remove(); }, 3000); } document.addEventListener("keydown", (event) => { if (event.key === "Escape") { const modal = document.querySelector(".modal-overlay"); if (modal) { modal.remove(); } } }); function initBotOptionsUI(scope) { const root = scope instanceof Element ? scope : document; const forms = root.querySelectorAll("[data-bot-options-form]"); forms.forEach((form) => { if (form.dataset.optionsInitialized === "1") { return; } form.dataset.optionsInitialized = "1"; bindSyncedPercentInputs(form); bindPresetButtons(form); bindChannelManager(form); bindDynamicState(form); updateDynamicState(form); }); } function bindSyncedPercentInputs(form) { const ranges = form.querySelectorAll("[data-options-range]"); ranges.forEach((range) => { const key = range.dataset.optionsRange; if (!key) { return; } const numberInput = form.querySelector(`[data-options-number="${key}"]`); const valueLabel = form.querySelector(`[data-options-value="${key}"]`); const syncToValue = (rawValue, fromNumberInput) => { const parsed = Number.parseInt(String(rawValue), 10); const safe = Number.isFinite(parsed) ? Math.max(0, Math.min(100, parsed)) : 0; range.value = String(safe); if (numberInput) { numberInput.value = String(safe); } if (valueLabel) { valueLabel.textContent = `${safe}%`; } if (fromNumberInput && document.activeElement !== numberInput) { numberInput.value = String(safe); } updateDynamicState(form); }; range.addEventListener("input", () => syncToValue(range.value, false)); if (numberInput) { numberInput.addEventListener("input", () => syncToValue(numberInput.value, true)); numberInput.addEventListener("blur", () => syncToValue(numberInput.value, true)); } }); } function bindPresetButtons(form) { const buttons = form.parentElement?.querySelectorAll("[data-options-preset]") || []; buttons.forEach((button) => { button.addEventListener("click", () => { const presetKey = button.dataset.optionsPreset; const preset = BOT_OPTIONS_PRESETS[presetKey] || null; if (!preset) { return; } setRadioValue(form, "response_mode", preset.response_mode); setPercentValue(form, "free_will_chance", preset.free_will_chance); setPercentValue(form, "memory_chance", preset.memory_chance); setPercentValue(form, "mention_probability", preset.mention_probability); setCheckboxValue(form, "gif_search_enabled", preset.gif_search_enabled); setCheckboxValue(form, "image_gen_enabled", preset.image_gen_enabled); setCheckboxValue(form, "nsfw_image_enabled", preset.nsfw_image_enabled); setCheckboxValue(form, "spontaneous_posts_enabled", preset.spontaneous_posts_enabled); updateDynamicState(form); }); }); } function bindDynamicState(form) { form.addEventListener("change", () => { updateDynamicState(form); }); } async function bindChannelManager(form) { const manager = form.querySelector("[data-channel-manager]"); if (!manager || manager.dataset.channelManagerInitialized === "1") { return; } manager.dataset.channelManagerInitialized = "1"; const guildId = manager.dataset.guildId; const restrictedInput = form.querySelector("[data-restricted-channel-input]"); const spontaneousInput = form.querySelector("[data-spontaneous-channel-input]"); const loadingNode = manager.querySelector("[data-channel-loading]"); const contentNode = manager.querySelector("[data-channel-content]"); const searchInput = manager.querySelector("[data-channel-search]"); const restrictedList = manager.querySelector("[data-restricted-channel-list]"); const spontaneousList = manager.querySelector("[data-spontaneous-channel-list]"); if ( !guildId || !restrictedInput || !spontaneousInput || !loadingNode || !contentNode || !searchInput || !restrictedList || !spontaneousList ) { return; } loadingNode.classList.remove("hidden"); contentNode.classList.add("hidden"); try { const response = await fetch(`/api/guilds/${guildId}/channels`, { credentials: "same-origin", }); if (!response.ok) { throw new Error(`Failed to load channels (${response.status})`); } const channels = await response.json(); if (!Array.isArray(channels)) { throw new Error("Invalid channel payload"); } const selectedSpontaneous = parseChannelIdsFromText(spontaneousInput.value); const render = () => { const query = searchInput.value.trim().toLowerCase(); const filtered = channels.filter((channel) => { const haystack = `${channel.name} ${channel.id}`.toLowerCase(); return !query || haystack.includes(query); }); renderRestrictedChannelButtons({ listNode: restrictedList, channels: filtered, selectedChannelId: restrictedInput.value.trim(), onSelect: (channel) => { restrictedInput.value = restrictedInput.value === channel.id ? "" : channel.id; render(); }, }); renderSpontaneousChannelButtons({ listNode: spontaneousList, channels: filtered, selectedIds: selectedSpontaneous, onToggle: (channel) => { if (selectedSpontaneous.has(channel.id)) { selectedSpontaneous.delete(channel.id); } else { selectedSpontaneous.add(channel.id); } spontaneousInput.value = [...selectedSpontaneous].join("\n"); render(); }, }); }; searchInput.addEventListener("input", render); render(); loadingNode.classList.add("hidden"); contentNode.classList.remove("hidden"); } catch { loadingNode.textContent = "Failed to load channels."; loadingNode.classList.remove("text-slate-400"); loadingNode.classList.add("text-rose-300"); } } function renderRestrictedChannelButtons({ listNode, channels, selectedChannelId, onSelect }) { listNode.replaceChildren(); const clearButton = document.createElement("button"); clearButton.type = "button"; clearButton.textContent = "Allow all channels"; clearButton.className = selectedChannelId.length === 0 ? "rounded-lg border border-indigo-500 bg-indigo-500/20 px-2.5 py-1.5 text-xs font-medium text-indigo-200" : "rounded-lg border border-slate-700 bg-slate-900 px-2.5 py-1.5 text-xs font-medium text-slate-200 hover:bg-slate-800"; clearButton.addEventListener("click", () => { onSelect({ id: "" }); }); listNode.appendChild(clearButton); channels.forEach((channel) => { const button = document.createElement("button"); button.type = "button"; button.textContent = `#${channel.name}`; const isSelected = selectedChannelId === channel.id; const isDisabled = !channel.writable; button.disabled = isDisabled; button.className = isSelected ? "rounded-lg border border-indigo-500 bg-indigo-500/20 px-2.5 py-1.5 text-xs font-medium text-indigo-200" : isDisabled ? "rounded-lg border border-slate-800 bg-slate-900/40 px-2.5 py-1.5 text-xs font-medium text-slate-500" : "rounded-lg border border-slate-700 bg-slate-900 px-2.5 py-1.5 text-xs font-medium text-slate-200 hover:bg-slate-800"; button.title = isDisabled ? "Bot cannot send messages in this channel" : `${channel.name} (${channel.id})`; button.addEventListener("click", () => onSelect(channel)); listNode.appendChild(button); }); if (channels.length === 0) { const empty = document.createElement("p"); empty.className = "w-full text-xs text-slate-500"; empty.textContent = "No channels match your search."; listNode.appendChild(empty); } } function renderSpontaneousChannelButtons({ listNode, channels, selectedIds, onToggle }) { listNode.replaceChildren(); channels.forEach((channel) => { const button = document.createElement("button"); button.type = "button"; button.textContent = `#${channel.name}`; const isSelected = selectedIds.has(channel.id); const isDisabled = !channel.writable; button.disabled = isDisabled; button.className = isSelected ? "rounded-lg border border-emerald-500 bg-emerald-500/20 px-2.5 py-1.5 text-xs font-medium text-emerald-200" : isDisabled ? "rounded-lg border border-slate-800 bg-slate-900/40 px-2.5 py-1.5 text-xs font-medium text-slate-500" : "rounded-lg border border-slate-700 bg-slate-900 px-2.5 py-1.5 text-xs font-medium text-slate-200 hover:bg-slate-800"; button.title = isDisabled ? "Bot cannot send messages in this channel" : `${channel.name} (${channel.id})`; button.addEventListener("click", () => { onToggle(channel); }); listNode.appendChild(button); }); if (channels.length === 0) { const empty = document.createElement("p"); empty.className = "w-full text-xs text-slate-500"; empty.textContent = "No channels match your search."; listNode.appendChild(empty); } } function parseChannelIdsFromText(raw) { const set = new Set(); if (!raw || typeof raw !== "string") { return set; } raw .split(/[\s,]+/) .map((entry) => entry.trim()) .filter(Boolean) .forEach((channelId) => { set.add(channelId); }); return set; } function updateDynamicState(form) { const state = readFormState(form); const score = computeBehaviorScore(state); const scoreBar = form.parentElement?.querySelector("[data-options-score-bar]") || null; const scoreLabel = form.parentElement?.querySelector("[data-options-score-label]") || null; if (scoreBar) { scoreBar.style.width = `${score}%`; } if (scoreLabel) { scoreLabel.textContent = `${score}% ยท ${getBehaviorTier(score)}`; } const responseModeInputs = form.querySelectorAll("[data-options-response-mode]"); responseModeInputs.forEach((input) => { const wrapper = input.closest("label"); if (!wrapper) { return; } const isActive = input.checked; wrapper.classList.toggle("border-indigo-500", isActive); wrapper.classList.toggle("bg-indigo-500/10", isActive); wrapper.classList.toggle("border-slate-700", !isActive); wrapper.classList.toggle("bg-slate-900", !isActive); }); const freeWillRange = form.querySelector('[data-options-range="free_will_chance"]'); const freeWillNumber = form.querySelector('[data-options-number="free_will_chance"]'); const freeWillDisabled = state.response_mode !== "free-will"; if (freeWillRange) { freeWillRange.disabled = freeWillDisabled; } if (freeWillNumber) { freeWillNumber.disabled = freeWillDisabled; } const nsfwToggle = form.querySelector("[data-options-nsfw-toggle]"); const nsfwRow = form.querySelector("[data-options-nsfw-row]"); if (nsfwToggle && nsfwRow) { nsfwToggle.disabled = !state.image_gen_enabled; nsfwRow.classList.toggle("opacity-50", !state.image_gen_enabled); nsfwRow.classList.toggle("pointer-events-none", !state.image_gen_enabled); if (!state.image_gen_enabled) { nsfwToggle.checked = false; } } const intervalInputs = form.querySelectorAll("[data-options-interval]"); intervalInputs.forEach((input) => { input.disabled = !state.spontaneous_posts_enabled; }); } function readFormState(form) { return { response_mode: getCheckedValue(form, "response_mode", "free-will"), free_will_chance: getPercentValue(form, "free_will_chance"), memory_chance: getPercentValue(form, "memory_chance"), mention_probability: getPercentValue(form, "mention_probability"), gif_search_enabled: getCheckboxValue(form, "gif_search_enabled"), image_gen_enabled: getCheckboxValue(form, "image_gen_enabled"), nsfw_image_enabled: getCheckboxValue(form, "nsfw_image_enabled"), spontaneous_posts_enabled: getCheckboxValue(form, "spontaneous_posts_enabled"), }; } function computeBehaviorScore(state) { const modeScore = state.response_mode === "free-will" ? 18 : 6; const autonomyScore = Math.round(state.free_will_chance * 0.28); const memoryScore = Math.round(state.memory_chance * 0.2); const mentionScore = Math.round(state.mention_probability * 0.14); const mediaScore = (state.gif_search_enabled ? 8 : 0) + (state.image_gen_enabled ? 10 : 0) + (state.nsfw_image_enabled ? 8 : 0); const spontaneityScore = state.spontaneous_posts_enabled ? 12 : 0; return Math.max(0, Math.min(100, modeScore + autonomyScore + memoryScore + mentionScore + mediaScore + spontaneityScore)); } function getBehaviorTier(score) { if (score < 30) { return "Conservative"; } if (score < 60) { return "Balanced"; } if (score < 80) { return "Aggressive"; } return "Maximum Chaos"; } function getPercentValue(form, key) { const range = form.querySelector(`[data-options-range="${key}"]`); const parsed = Number.parseInt(range?.value || "0", 10); if (!Number.isFinite(parsed)) { return 0; } return Math.max(0, Math.min(100, parsed)); } function setPercentValue(form, key, value) { const safe = Math.max(0, Math.min(100, Number.parseInt(String(value), 10) || 0)); const range = form.querySelector(`[data-options-range="${key}"]`); const numberInput = form.querySelector(`[data-options-number="${key}"]`); const label = form.querySelector(`[data-options-value="${key}"]`); if (range) { range.value = String(safe); } if (numberInput) { numberInput.value = String(safe); } if (label) { label.textContent = `${safe}%`; } } function getCheckboxValue(form, key) { const input = form.querySelector(`[name="${key}"][type="checkbox"]`); return Boolean(input?.checked); } function setCheckboxValue(form, key, value) { const input = form.querySelector(`[name="${key}"][type="checkbox"]`); if (input) { input.checked = Boolean(value); } } function getCheckedValue(form, name, fallback) { const checked = form.querySelector(`[name="${name}"]:checked`); return checked?.value || fallback; } function setRadioValue(form, name, value) { const radio = form.querySelector(`[name="${name}"][value="${value}"]`); if (radio) { radio.checked = true; } } window.switchTab = switchTab; window.showNotification = showNotification;