1222 lines
52 KiB
HTML
1222 lines
52 KiB
HTML
<html lang="zh-CN" data-darkreader-mode="dynamic" data-darkreader-scheme="dark" data-darkreader-proxy-injected="true"><head>
|
||
<!-- 引入YAML解析库 -->
|
||
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
|
||
<!-- 引入jQuery库 -->
|
||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
primary: '#9d7cf5',
|
||
secondary: '#2d2447',
|
||
dark: '#2a1f3d',
|
||
light: '#1e1831',
|
||
lightBg: '#f8fafc',
|
||
lightCard: '#ffffff',
|
||
lightText: '#1e293b',
|
||
lightBorder: '#e2e8f0',
|
||
darkBorder: '#332a50',
|
||
jade: {
|
||
50: '#f0fdfa',
|
||
100: '#ccfbf1',
|
||
200: '#99f6e4',
|
||
300: '#5eead4',
|
||
400: '#2dd4bf',
|
||
500: '#14b8a6',
|
||
600: '#0d9488',
|
||
700: '#0f766e',
|
||
800: '#115e59',
|
||
900: '#134e4a',
|
||
},
|
||
classic: {
|
||
50: '#fefbf5',
|
||
100: '#fdf6e9',
|
||
200: '#faeed7',
|
||
300: '#f5dfb7',
|
||
400: '#e9c887',
|
||
500: '#d9a856',
|
||
600: '#c28c40',
|
||
700: '#a06c30',
|
||
800: '#7d5428',
|
||
900: '#644423',
|
||
},
|
||
romantic: {
|
||
50: '#fef7f7',
|
||
100: '#fdeaea',
|
||
200: '#fbdadb',
|
||
300: '#f7bfc1',
|
||
400: '#f097a0',
|
||
500: '#e36774',
|
||
600: '#d14455',
|
||
700: '#b12d3e',
|
||
800: '#942738',
|
||
900: '#7e2635',
|
||
},
|
||
fresh: {
|
||
50: '#f0fdf9',
|
||
100: '#dcfce7',
|
||
200: '#bbf7d0',
|
||
300: '#86efac',
|
||
400: '#4ade80',
|
||
500: '#22c55e',
|
||
600: '#16a34a',
|
||
700: '#15803d',
|
||
800: '#166534',
|
||
900: '#14532d',
|
||
},
|
||
},
|
||
fontFamily: {
|
||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||
serif: ['Georgia', 'Cambria', 'serif'],
|
||
},
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
</head>
|
||
<body class="status-only-mode romantic-mode" style="">
|
||
<!-- YAML格式数据源 -->
|
||
<script id="yaml-data-source" type="text/yaml"></script>
|
||
|
||
<!-- 全宽度平铺整个页面 -->
|
||
<div class="w-full px-3 py-4">
|
||
<!-- 整合后的状态栏区块 -->
|
||
<div class="status-block mb-4 rounded-2xl shadow-lg overflow-hidden border border-gray-700/30 theme-transition w-full">
|
||
<!-- 顶部信息和主题切换栏 -->
|
||
<div class="bg-gradient-to-r from-primary/20 to-primary/5 p-4 rounded-t-2xl border-b border-gray-700/20 theme-transition">
|
||
<div class="flex flex-col md:flex-row justify-between items-center gap-3">
|
||
<!-- 日期和地点信息 - 靠左显示 -->
|
||
<div class="flex flex-wrap items-center gap-x-4 text-sm text-gray-300 w-full md:w-auto">
|
||
<div class="flex items-center">
|
||
<span id="location-display" class="whitespace-nowrap">📍 加载中...</span>
|
||
</div>
|
||
<div class="flex items-center">
|
||
<span id="time-display" class="whitespace-nowrap">⏰ 加载中...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧设置按钮 -->
|
||
<div class="flex items-center">
|
||
<button id="settings-btn" class="theme-btn rounded-full border border-gray-700/50 hover:bg-primary/20 transition-colors" title="设置">
|
||
<i class="fa fa-cog"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文章正文区域 -->
|
||
<div id="maintext-container" class="px-4 py-3 text-base leading-relaxed w-full"><div class="loading-state"><i class="fa fa-spinner fa-spin mr-2"></i>加载中...</div></div>
|
||
<!-- 原始文本容器 -->
|
||
<div id="maintext">加载中...</div>
|
||
|
||
<!-- 角色状态详情 -->
|
||
<div class="border-t border-gray-700/20 theme-transition">
|
||
<details class="w-full group" open="">
|
||
<summary class="w-full px-4 py-3 font-semibold cursor-pointer flex justify-between items-center list-none hover:bg-primary/10 transition-colors theme-transition">
|
||
<span class="text-gray-300 flex items-center">
|
||
<i class="fa fa-users text-primary mr-2"></i>角色状态详情
|
||
</span>
|
||
<i class="fa fa-chevron-down text-primary transition-transform duration-300 group-open:rotate-180"></i>
|
||
</summary>
|
||
|
||
<div id="characters-container" class="p-3 space-y-3 overflow-hidden transition-all duration-300" style="max-height: 80px;"><div class="text-center py-4 text-gray-500 theme-transition">
|
||
<i class="fa fa-info-circle mr-1"></i>数据加载中...
|
||
</div></div>
|
||
</details>
|
||
</div>
|
||
|
||
<!-- 行动选项区域 -->
|
||
<div class="border-t border-gray-700/20 bg-gradient-to-b from-dark to-primary/10 rounded-b-2xl theme-transition">
|
||
<h3 class="px-4 pt-3 font-bold flex items-center" id="action-title">
|
||
<i class="fa fa-list-alt mr-2"></i>
|
||
<span id="action-owner">加载中...</span>的行动选项
|
||
</h3>
|
||
<div id="options-container" class="px-4 pb-4">
|
||
<ul id="options-list" class="list-none space-y-2 text-sm pl-1 py-2"><li class="text-gray-400"><div class="flex items-center"><div class="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-primary mr-2"></div>加载选项中...</div></li></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 错误弹窗 -->
|
||
<div id="error-modal" class="settings-panel">
|
||
<div class="settings-content theme-transition">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h2 class="text-lg font-bold text-red-400 flex items-center">
|
||
<i class="fa fa-exclamation-triangle mr-2"></i>
|
||
状态栏渲染失败
|
||
</h2>
|
||
<button id="error-close" class="theme-btn rounded-full hover:bg-gray-700/30" title="关闭">
|
||
<i class="fa fa-times"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="mb-4">
|
||
<p class="text-gray-300 mb-3">可能掉格式了,请检查AI输出。</p>
|
||
<div class="bg-red-900/20 border border-red-800/30 rounded-lg p-3">
|
||
<label class="text-sm font-medium text-red-400 block mb-2">错误详情:</label>
|
||
<div id="error-details" class="text-red-300 text-sm font-mono bg-black/20 p-2 rounded border overflow-auto max-h-32"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex justify-end">
|
||
<button id="error-confirm" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
|
||
确定
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设置面板 -->
|
||
<div id="settings-panel" class="settings-panel" style="display: none;">
|
||
<div class="settings-content theme-transition">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h2 class="text-lg font-bold text-gray-300">设置</h2>
|
||
<button id="settings-close" class="theme-btn rounded-full hover:bg-gray-700/30" title="关闭">
|
||
<i class="fa fa-times"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 主题设置 -->
|
||
<div class="settings-group">
|
||
<label class="settings-label">主题选择</label>
|
||
<div class="theme-grid">
|
||
<div class="theme-option active" data-theme="romantic" title="暧昧风格">
|
||
<i class="fa fa-heart mr-2"></i>
|
||
<span>暧昧</span>
|
||
</div>
|
||
<div class="theme-option" data-theme="day" title="白天模式">
|
||
<i class="fa fa-sun-o mr-2"></i>
|
||
<span>白天</span>
|
||
</div>
|
||
<div class="theme-option" data-theme="jade" title="青玉模式">
|
||
<i class="fa fa-leaf mr-2"></i>
|
||
<span>青玉</span>
|
||
</div>
|
||
<div class="theme-option" data-theme="classic" title="古典模式">
|
||
<i class="fa fa-book mr-2"></i>
|
||
<span>古典</span>
|
||
</div>
|
||
<div class="theme-option" data-theme="night" title="黑夜模式">
|
||
<i class="fa fa-moon-o mr-2"></i>
|
||
<span>黑夜</span>
|
||
</div>
|
||
<div class="theme-option" data-theme="fresh" title="小清新风格">
|
||
<i class="fa fa-envira mr-2"></i>
|
||
<span>小清新</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 显示模式设置 -->
|
||
<div class="settings-group">
|
||
<label class="settings-label">显示模式</label>
|
||
<div class="display-mode-options">
|
||
<label class="display-mode-option">
|
||
<input type="radio" name="display-mode" value="integrated" id="integrated-mode">
|
||
<span class="radio-custom"></span>
|
||
<div class="option-content">
|
||
<span class="option-title">一体式美化</span>
|
||
<span class="option-desc">显示完整的故事内容和状态信息</span>
|
||
</div>
|
||
</label>
|
||
<label class="display-mode-option">
|
||
<input type="radio" name="display-mode" value="status-only" id="status-only-mode" checked="">
|
||
<span class="radio-custom"></span>
|
||
<div class="option-content">
|
||
<span class="option-title">美化状态栏</span>
|
||
<span class="option-desc">仅显示状态信息,隐藏正文内容</span>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 行动选项操作设置 -->
|
||
<div class="settings-group">
|
||
<label class="settings-label">行动选项点击处理</label>
|
||
<div class="action-mode-options">
|
||
<label class="action-mode-option">
|
||
<input type="radio" name="action-mode" value="send-to-chat" id="send-to-chat-mode" checked="">
|
||
<span class="radio-custom"></span>
|
||
<div class="option-content">
|
||
<span class="option-title">发送到酒馆聊天框</span>
|
||
<span class="option-desc">点击后将选项文本添加到聊天输入框中</span>
|
||
</div>
|
||
</label>
|
||
<label class="action-mode-option">
|
||
<input type="radio" name="action-mode" value="direct-execute" id="direct-execute-mode">
|
||
<span class="radio-custom"></span>
|
||
<div class="option-content">
|
||
<span class="option-title">点击直接执行</span>
|
||
<span class="option-desc">点击后立即发送消息并触发回复</span>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 自动折叠设置 -->
|
||
<div class="settings-group">
|
||
<label class="settings-label">角色状态详情</label>
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-sm text-gray-400">自动折叠</span>
|
||
<label class="settings-switch">
|
||
<input type="checkbox" id="auto-collapse-toggle" checked="">
|
||
<span class="settings-slider"></span>
|
||
</label>
|
||
</div>
|
||
<div class="text-xs text-gray-500 mt-1">开启时默认折叠角色状态详情,关闭时默认展开</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 封装与SillyTavern通信的函数
|
||
const triggerQuickReply = text => {
|
||
// 检查文本有效性,避免发送空内容
|
||
if (!text || ['…', '...'].includes(text.trim()) || text.trim().length === 0) return;
|
||
// 检查SillyTavern环境并发送命令
|
||
if (typeof triggerSlash === 'function') {
|
||
triggerSlash(`/send ${text}|/trigger`);
|
||
} else {
|
||
console.log('SillyTavern environment not detected. Would send:', text);
|
||
}
|
||
};
|
||
|
||
// 发送文本到酒馆聊天框的函数
|
||
const sendToChatBox = text => {
|
||
// 检查文本有效性
|
||
if (!text || ['…', '...'].includes(text.trim()) || text.trim().length === 0) return;
|
||
|
||
try {
|
||
// 使用jQuery获取父页面的聊天输入框
|
||
const $textarea = $(parent.document).find('#send_textarea');
|
||
|
||
if ($textarea.length === 0) {
|
||
console.log('未找到聊天输入框 (#send_textarea)');
|
||
return;
|
||
}
|
||
|
||
// 获取当前输入框的内容
|
||
const currentContent = $textarea.val() || '';
|
||
|
||
// 检查是否已经包含该文本
|
||
if (currentContent.includes(text.trim())) {
|
||
console.log('聊天框中已包含该文本,跳过添加');
|
||
return;
|
||
}
|
||
|
||
// 如果输入框不为空,添加换行符分隔
|
||
const separator = currentContent.trim() ? '\n' : '';
|
||
const newContent = currentContent + separator + text.trim();
|
||
|
||
// 设置新内容
|
||
$textarea.val(newContent);
|
||
|
||
// 触发input事件,确保相关监听器能够响应
|
||
$textarea.trigger('input');
|
||
|
||
console.log('已添加文本到聊天框:', text.trim());
|
||
} catch (error) {
|
||
console.error('发送到聊天框时出错:', error);
|
||
// 如果出错,回退到直接执行
|
||
triggerQuickReply(text);
|
||
}
|
||
};
|
||
|
||
// 页面加载完成后执行
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// 初始化设置面板功能
|
||
initSettingsPanel();
|
||
|
||
// 初始化错误弹窗功能
|
||
initErrorModal();
|
||
|
||
// 初始化主题切换功能
|
||
initThemeToggle();
|
||
|
||
// 处理文本格式化
|
||
formatMainText();
|
||
|
||
// 初始化YAML数据源的渲染器
|
||
const storyRenderer = new StoryRenderer('yaml-data-source');
|
||
storyRenderer.init();
|
||
|
||
// 页面加载完成后的显示模式切换测试
|
||
|
||
// 默认设置为美化状态栏模式
|
||
console.log('设置为美化状态栏模式...');
|
||
updateDisplayMode('status-only');
|
||
|
||
// 为行动选项添加事件委托
|
||
document.getElementById('options-list').addEventListener('click', event => {
|
||
// 检查点击的是否是选项列表项
|
||
if (event.target.tagName === 'LI') {
|
||
// 获取文本内容并清除首尾空格
|
||
const optionText = event.target.textContent.trim();
|
||
// 移除选项前的数字编号和点号(如"1. ")
|
||
const cleanedText = optionText.replace(/^\d+\.\s*/, '');
|
||
|
||
// 根据设置选择操作方式
|
||
const actionMode = getActionMode();
|
||
if (actionMode === 'send-to-chat') {
|
||
sendToChatBox(cleanedText);
|
||
} else {
|
||
triggerQuickReply(cleanedText);
|
||
}
|
||
|
||
// 视觉反馈:短暂高亮选中的选项
|
||
const originalBg = event.target.style.backgroundColor;
|
||
event.target.style.backgroundColor = 'rgba(157, 124, 245, 0.2)';
|
||
setTimeout(() => {
|
||
event.target.style.backgroundColor = originalBg;
|
||
}, 300);
|
||
}
|
||
});
|
||
|
||
// 获取当前行动模式设置的函数
|
||
function getActionMode() {
|
||
const savedMode = localStorage.getItem('actionMode');
|
||
return savedMode || 'send-to-chat'; // 默认为发送到聊天框
|
||
}
|
||
});
|
||
|
||
// 文本格式化处理 - 修复了中英文双引号的样式替换
|
||
function formatMainText() {
|
||
// 获取原始文本容器和显示容器
|
||
const maintextElement = document.getElementById('maintext');
|
||
const maintextContainer = document.getElementById('maintext-container');
|
||
|
||
try {
|
||
// 获取原始文本内容
|
||
let text = maintextElement.textContent || '';
|
||
|
||
// 检查是否为空内容,如果为空则显示加载中状态
|
||
if (!text.trim()) {
|
||
maintextElement.textContent = '加载中...';
|
||
maintextContainer.innerHTML =
|
||
'<div class="loading-state"><i class="fa fa-spinner fa-spin mr-2"></i>加载中...</div>';
|
||
return;
|
||
}
|
||
|
||
// 1. 处理英文双引号
|
||
const englishDoubleQuoteRegex = /"([^"\\]*(?:\\.[^"\\]*)*)"/g;
|
||
text = text.replace(englishDoubleQuoteRegex, (match, content) => {
|
||
return `<span class="double-quoted">"</span><span class="double-quoted">${content}</span><span class="double-quoted">"</span>`;
|
||
});
|
||
|
||
// 2. 处理中文双引号(左引号和右引号)
|
||
const chineseLeftQuoteRegex = /"([^"]*?)"/g;
|
||
text = text.replace(chineseLeftQuoteRegex, (match, content) => {
|
||
return `<span class="double-quoted">"</span><span class="double-quoted">${content}</span><span class="double-quoted">"</span>`;
|
||
});
|
||
|
||
// 3. 处理单引号
|
||
const singleQuoteRegex = /'([^'\\]*(?:\\.[^'\\]*)*)'/g;
|
||
text = text.replace(singleQuoteRegex, (match, content) => {
|
||
return `<span class="single-quoted">'</span><span class="single-quoted">${content}</span><span class="single-quoted">'</span>`;
|
||
});
|
||
|
||
// 4. 处理单星号
|
||
const asteriskRegex = /\*([^\*]+)\*/g;
|
||
text = text.replace(asteriskRegex, (match, content) => {
|
||
return `<span class="asterisk-quoted">${content}</span>`;
|
||
});
|
||
|
||
// 5. 处理分段
|
||
const paragraphs = text
|
||
.replace(/\n\s*\n/g, '\n\n')
|
||
.split(/\n\s*\n/)
|
||
.filter(paragraph => paragraph.trim().length > 0);
|
||
|
||
// 6. 包装成段落元素
|
||
const formattedParagraphs = paragraphs.map(paragraph => {
|
||
return `<p class="paragraph">${paragraph}</p>`;
|
||
});
|
||
|
||
// 7. 放入显示容器
|
||
maintextContainer.innerHTML = formattedParagraphs.join('');
|
||
} catch (error) {
|
||
console.error('正文渲染失败:', error);
|
||
// 当渲染失败时,显示错误信息
|
||
maintextContainer.innerHTML =
|
||
'<div class="error-state"><i class="fa fa-exclamation-triangle mr-2"></i>渲染正文失败,请检查正文是否被 <maintext>正文</maintext>包裹.</div>';
|
||
}
|
||
}
|
||
|
||
// 初始化错误弹窗功能
|
||
function initErrorModal() {
|
||
const errorModal = document.getElementById('error-modal');
|
||
const errorClose = document.getElementById('error-close');
|
||
const errorConfirm = document.getElementById('error-confirm');
|
||
|
||
// 关闭错误弹窗
|
||
const closeErrorModal = () => {
|
||
errorModal.style.display = 'none';
|
||
document.body.style.overflow = '';
|
||
};
|
||
|
||
errorClose.addEventListener('click', closeErrorModal);
|
||
errorConfirm.addEventListener('click', closeErrorModal);
|
||
|
||
// 点击弹窗外部关闭
|
||
errorModal.addEventListener('click', e => {
|
||
if (e.target === errorModal) {
|
||
closeErrorModal();
|
||
}
|
||
});
|
||
|
||
// ESC键关闭
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape' && errorModal.style.display === 'flex') {
|
||
closeErrorModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 显示错误弹窗
|
||
function showErrorModal(errorMessage) {
|
||
const errorModal = document.getElementById('error-modal');
|
||
const errorDetails = document.getElementById('error-details');
|
||
|
||
errorDetails.textContent = errorMessage;
|
||
errorModal.style.display = 'flex';
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
// 初始化设置面板功能
|
||
function initSettingsPanel() {
|
||
const settingsBtn = document.getElementById('settings-btn');
|
||
const settingsPanel = document.getElementById('settings-panel');
|
||
const settingsClose = document.getElementById('settings-close');
|
||
const autoCollapseToggle = document.getElementById('auto-collapse-toggle');
|
||
const displayModeRadios = document.querySelectorAll('input[name="display-mode"]');
|
||
const actionModeRadios = document.querySelectorAll('input[name="action-mode"]');
|
||
|
||
// 显示设置面板
|
||
settingsBtn.addEventListener('click', () => {
|
||
settingsPanel.style.display = 'flex';
|
||
document.body.style.overflow = 'hidden';
|
||
});
|
||
|
||
// 关闭设置面板
|
||
const closeSettings = () => {
|
||
settingsPanel.style.display = 'none';
|
||
document.body.style.overflow = '';
|
||
};
|
||
|
||
settingsClose.addEventListener('click', closeSettings);
|
||
|
||
// 点击面板外部关闭
|
||
settingsPanel.addEventListener('click', e => {
|
||
if (e.target === settingsPanel) {
|
||
closeSettings();
|
||
}
|
||
});
|
||
|
||
// ESC键关闭
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape' && settingsPanel.style.display === 'flex') {
|
||
closeSettings();
|
||
}
|
||
});
|
||
|
||
// 初始化显示模式设置
|
||
const savedDisplayMode = localStorage.getItem('displayMode') || 'status-only';
|
||
displayModeRadios.forEach(radio => {
|
||
if (radio.value === savedDisplayMode) {
|
||
radio.checked = true;
|
||
}
|
||
radio.addEventListener('change', () => {
|
||
if (radio.checked) {
|
||
updateDisplayMode(radio.value);
|
||
localStorage.setItem('displayMode', radio.value);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 应用保存的显示模式
|
||
updateDisplayMode(savedDisplayMode);
|
||
|
||
// 初始化行动模式设置
|
||
const savedActionMode = localStorage.getItem('actionMode') || 'send-to-chat';
|
||
actionModeRadios.forEach(radio => {
|
||
if (radio.value === savedActionMode) {
|
||
radio.checked = true;
|
||
}
|
||
radio.addEventListener('change', () => {
|
||
if (radio.checked) {
|
||
localStorage.setItem('actionMode', radio.value);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 初始化自动折叠设置
|
||
const savedAutoCollapse = localStorage.getItem('autoCollapse');
|
||
if (savedAutoCollapse !== null) {
|
||
autoCollapseToggle.checked = savedAutoCollapse === 'true';
|
||
}
|
||
|
||
// 监听自动折叠设置变化
|
||
autoCollapseToggle.addEventListener('change', () => {
|
||
localStorage.setItem('autoCollapse', autoCollapseToggle.checked);
|
||
// 立即应用设置到当前的角色状态详情
|
||
updateCharacterDetailsState();
|
||
});
|
||
}
|
||
|
||
// 更新显示模式
|
||
function updateDisplayMode(mode) {
|
||
const bodyElement = document.body;
|
||
|
||
// 移除之前的显示模式类
|
||
bodyElement.classList.remove('status-only-mode');
|
||
|
||
// 根据模式添加相应的类
|
||
if (mode === 'status-only') {
|
||
bodyElement.classList.add('status-only-mode');
|
||
}
|
||
}
|
||
|
||
// 更新角色状态详情的展开/折叠状态
|
||
function updateCharacterDetailsState() {
|
||
const detailsElement = document.querySelector('details');
|
||
const autoCollapseToggle = document.getElementById('auto-collapse-toggle');
|
||
|
||
if (detailsElement && autoCollapseToggle) {
|
||
detailsElement.open = !autoCollapseToggle.checked;
|
||
}
|
||
}
|
||
|
||
// 初始化主题切换功能
|
||
function initThemeToggle() {
|
||
const themeOptions = document.querySelectorAll('.theme-option');
|
||
const bodyElement = document.body;
|
||
|
||
// 检查本地存储中的主题偏好
|
||
const savedTheme = localStorage.getItem('storyTheme') || 'romantic';
|
||
switchToTheme(savedTheme);
|
||
|
||
// 绑定点击事件
|
||
themeOptions.forEach(option => {
|
||
option.addEventListener('click', () => {
|
||
const theme = option.getAttribute('data-theme');
|
||
switchToTheme(theme);
|
||
localStorage.setItem('storyTheme', theme);
|
||
});
|
||
});
|
||
|
||
// 切换到指定主题
|
||
function switchToTheme(theme) {
|
||
// 移除所有主题类
|
||
bodyElement.classList.remove(
|
||
'night-mode',
|
||
'day-mode',
|
||
'jade-mode',
|
||
'classic-mode',
|
||
'romantic-mode',
|
||
'fresh-mode',
|
||
);
|
||
|
||
// 添加选中主题类
|
||
bodyElement.classList.add(`${theme}-mode`);
|
||
|
||
// 更新设置面板中的选项状态
|
||
themeOptions.forEach(option => {
|
||
if (option.getAttribute('data-theme') === theme) {
|
||
option.classList.add('active');
|
||
} else {
|
||
option.classList.remove('active');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 故事渲染器类
|
||
class StoryRenderer {
|
||
constructor(dataSourceId) {
|
||
this.dataSourceId = dataSourceId;
|
||
this.yamlData = null;
|
||
this.rootNode = null; // 根节点名称
|
||
this.initElements();
|
||
}
|
||
|
||
// 初始化DOM元素引用
|
||
initElements() {
|
||
this.elements = {
|
||
timeDisplay: document.getElementById('time-display'),
|
||
locationDisplay: document.getElementById('location-display'),
|
||
charactersContainer: document.getElementById('characters-container'),
|
||
actionOwner: document.getElementById('action-owner'),
|
||
optionsList: document.getElementById('options-list'),
|
||
};
|
||
}
|
||
|
||
// 初始化方法
|
||
init() {
|
||
try {
|
||
// 从script标签中加载YAML数据
|
||
this.loadYamlFromScriptTag();
|
||
|
||
// 如果没有有效数据(显示加载状态的情况),直接返回
|
||
if (!this.yamlData) {
|
||
this.setupEventListeners();
|
||
return;
|
||
}
|
||
|
||
// 找到根节点
|
||
this.findRootNode();
|
||
this.renderAll();
|
||
this.setupEventListeners();
|
||
} catch (error) {
|
||
this.handleError(error);
|
||
}
|
||
}
|
||
|
||
// 从script标签加载并解析YAML数据
|
||
loadYamlFromScriptTag() {
|
||
const scriptElement = document.getElementById(this.dataSourceId);
|
||
if (!scriptElement) {
|
||
throw new Error('未找到id为"yaml-data-source"的script标签');
|
||
}
|
||
|
||
let yamlContent = scriptElement.textContent.trim();
|
||
|
||
// 检查是否为真正的空内容
|
||
if (!yamlContent) {
|
||
// 当YAML内容为空时,设置默认的加载状态但不抛出错误
|
||
this.showLoadingState();
|
||
return; // 直接返回,不抛出错误
|
||
}
|
||
|
||
// 如果是"加载中..."文本,也显示加载状态
|
||
if (yamlContent === '加载中...') {
|
||
this.showLoadingState();
|
||
return;
|
||
}
|
||
|
||
// 有内容,尝试解析YAML
|
||
try {
|
||
this.yamlData = jsyaml.load(yamlContent);
|
||
} catch (e) {
|
||
// YAML格式错误,应该弹出错误对话框
|
||
throw new Error(`YAML格式错误: ${e.message}`);
|
||
}
|
||
|
||
if (!this.yamlData || Object.keys(this.yamlData).length === 0) {
|
||
// 解析成功但数据为空,这是格式问题
|
||
throw new Error('YAML解析成功但数据为空,请检查YAML格式是否正确');
|
||
}
|
||
}
|
||
|
||
// 显示加载状态的独立方法
|
||
showLoadingState() {
|
||
this.elements.timeDisplay.textContent = '⏰ 加载中...';
|
||
this.elements.locationDisplay.textContent = '📍 加载中...';
|
||
this.elements.actionOwner.textContent = '加载中...';
|
||
this.elements.charactersContainer.innerHTML = this.createEmptyState('数据加载中...');
|
||
this.elements.optionsList.innerHTML =
|
||
'<li class="text-gray-400"><div class="flex items-center"><div class="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-primary mr-2"></div>加载选项中...</div></li>';
|
||
}
|
||
|
||
// 查找根节点
|
||
findRootNode() {
|
||
const rootNodeNames = Object.keys(this.yamlData);
|
||
if (rootNodeNames.length === 0) {
|
||
throw new Error('YAML数据中未找到任何根节点');
|
||
}
|
||
|
||
this.rootNode = rootNodeNames[0];
|
||
}
|
||
|
||
// 格式化节点名称,使其更易读
|
||
formatNodeName(name) {
|
||
// 提取emoji后面的文本(如果有emoji)
|
||
const emojiMatch = name.match(/^(\p{Emoji}\s*)(.*)$/u);
|
||
if (emojiMatch && emojiMatch[2]) {
|
||
return emojiMatch[2];
|
||
}
|
||
return name;
|
||
}
|
||
|
||
// 渲染所有内容
|
||
renderAll() {
|
||
if (!this.rootNode || !this.yamlData[this.rootNode]) {
|
||
throw new Error('未找到有效的根节点数据');
|
||
}
|
||
|
||
const rootData = this.yamlData[this.rootNode];
|
||
this.renderHeaderInfo(rootData);
|
||
this.renderCharacters(rootData);
|
||
this.renderActionOptions(rootData);
|
||
}
|
||
|
||
// 渲染头部信息(日期和时间和地点)
|
||
renderHeaderInfo(rootData) {
|
||
// 查找日期时间字段
|
||
const dateTimeField = this.findFieldByKeywords(rootData, ['日期', '时间', 'datetime', 'time']);
|
||
// 查找地点字段
|
||
const locationField = this.findFieldByKeywords(rootData, ['地点', '位置', 'location', 'place']);
|
||
|
||
// 直接使用包含emoji的值
|
||
this.elements.timeDisplay.textContent = dateTimeField ? rootData[dateTimeField] : '⏰ 时间未知';
|
||
this.elements.locationDisplay.textContent = locationField ? rootData[locationField] : '📍 地点未知';
|
||
}
|
||
|
||
// 根据关键词查找字段名
|
||
findFieldByKeywords(data, keywords) {
|
||
if (!data || typeof data !== 'object') return null;
|
||
|
||
const fields = Object.keys(data);
|
||
for (const field of fields) {
|
||
for (const keyword of keywords) {
|
||
if (field.toLowerCase().includes(keyword.toLowerCase())) {
|
||
return field;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 渲染角色列表
|
||
renderCharacters(rootData) {
|
||
// 查找用户列表字段
|
||
const userListField = this.findFieldByKeywords(rootData, ['用户', '角色', '列表', 'user', 'role', 'list']);
|
||
const userList = userListField && Array.isArray(rootData[userListField]) ? rootData[userListField] : [];
|
||
|
||
this.elements.charactersContainer.innerHTML = '';
|
||
|
||
if (userList.length === 0) {
|
||
this.elements.charactersContainer.innerHTML = this.createEmptyState('没有角色数据');
|
||
return;
|
||
}
|
||
|
||
// 处理每个用户项
|
||
userList.forEach(userItem => {
|
||
// 检查是否有外层包装
|
||
let userData = userItem;
|
||
|
||
if (typeof userItem === 'object' && userItem !== null) {
|
||
const userField = this.findFieldByKeywords(userItem, ['用户', 'user', '角色', 'role']);
|
||
if (userField) {
|
||
userData = userItem[userField];
|
||
}
|
||
}
|
||
|
||
const characterCard = this.createCharacterCard(userData);
|
||
if (characterCard) {
|
||
this.elements.charactersContainer.appendChild(characterCard);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 创建单个角色卡片
|
||
createCharacterCard(userData) {
|
||
if (!userData || typeof userData !== 'object') return null;
|
||
|
||
const card = document.createElement('div');
|
||
card.className =
|
||
'bg-dark rounded-xl border border-gray-700/30 p-3.5 shadow-sm card-hover character-card theme-transition';
|
||
|
||
// 查找名字字段
|
||
const nameField = this.findFieldByKeywords(userData, ['名字', '姓名', '名称', 'name']);
|
||
const userName = nameField ? userData[nameField] : '👤 未知角色';
|
||
|
||
// 创建标题
|
||
const title = document.createElement('h3');
|
||
title.className = 'font-bold text-lg mb-2 pb-1 border-b border-gray-700/30 theme-transition';
|
||
title.textContent = `${this.formatNodeName(userName)}的状态`;
|
||
card.appendChild(title);
|
||
|
||
// 创建属性列表
|
||
const attributesList = document.createElement('ul');
|
||
attributesList.className = 'space-y-2 text-sm';
|
||
card.appendChild(attributesList);
|
||
|
||
// 处理所有属性
|
||
Object.keys(userData).forEach(key => {
|
||
// 跳过已经作为标题使用的名字节点
|
||
if (key === nameField) return;
|
||
|
||
// 创建属性项,直接使用包含emoji的值
|
||
const attributeItem = this.createAttributeItem(key, userData[key]);
|
||
if (attributeItem) {
|
||
attributesList.appendChild(attributeItem);
|
||
}
|
||
});
|
||
|
||
return card;
|
||
}
|
||
|
||
// 创建属性项
|
||
createAttributeItem(key, value) {
|
||
const item = document.createElement('li');
|
||
|
||
// 处理数组类型
|
||
if (Array.isArray(value)) {
|
||
item.innerHTML = `<span class="font-medium text-primary">${this.formatNodeName(key)}:</span>`;
|
||
|
||
const subList = document.createElement('ul');
|
||
subList.className = 'list-disc list-inside ml-4 mt-1 space-y-1 text-gray-400 theme-transition';
|
||
|
||
value.forEach(itemData => {
|
||
if (typeof itemData === 'object' && itemData !== null) {
|
||
const subKey = Object.keys(itemData)[0];
|
||
const subValue = itemData[subKey];
|
||
const subItem = document.createElement('li');
|
||
subItem.textContent = subValue;
|
||
subList.appendChild(subItem);
|
||
} else {
|
||
const subItem = document.createElement('li');
|
||
subItem.textContent = itemData;
|
||
subList.appendChild(subItem);
|
||
}
|
||
});
|
||
|
||
item.appendChild(subList);
|
||
}
|
||
// 处理对象类型
|
||
else if (typeof value === 'object' && value !== null) {
|
||
item.innerHTML = `<span class="font-medium text-primary">${this.formatNodeName(key)}:</span>`;
|
||
|
||
const subList = document.createElement('ul');
|
||
subList.className = 'list-disc list-inside ml-4 mt-1 space-y-1 text-gray-400 theme-transition';
|
||
|
||
Object.keys(value).forEach(subKey => {
|
||
const subItem = document.createElement('li');
|
||
subItem.textContent = value[subKey];
|
||
subList.appendChild(subItem);
|
||
});
|
||
|
||
item.appendChild(subList);
|
||
}
|
||
// 处理普通文本值
|
||
else if (value !== null && value !== undefined && value.toString().trim() !== '') {
|
||
item.innerHTML = `<span class="font-medium text-primary">${this.formatNodeName(key)}:</span> ${value}`;
|
||
}
|
||
|
||
return item;
|
||
}
|
||
|
||
// 渲染行动选项
|
||
renderActionOptions(rootData) {
|
||
// 查找行动选项字段
|
||
const actionOptionsField = this.findFieldByKeywords(rootData, ['行动', '选项', 'action', 'option']);
|
||
const actionOptions =
|
||
actionOptionsField && typeof rootData[actionOptionsField] === 'object' ? rootData[actionOptionsField] : {};
|
||
|
||
// 设置行动所有者
|
||
const ownerField = this.findFieldByKeywords(actionOptions, ['名字', '姓名', '所有者', 'owner', 'name']);
|
||
this.elements.actionOwner.textContent = ownerField
|
||
? this.formatNodeName(actionOptions[ownerField])
|
||
: '未知角色';
|
||
|
||
// 渲染选项列表
|
||
const optionsField = this.findFieldByKeywords(actionOptions, ['选项', '选择', 'option', 'choice']);
|
||
const options = optionsField && Array.isArray(actionOptions[optionsField]) ? actionOptions[optionsField] : [];
|
||
|
||
this.elements.optionsList.innerHTML = '';
|
||
|
||
if (options.length === 0) {
|
||
this.elements.optionsList.innerHTML = this.createEmptyState('没有可用选项');
|
||
return;
|
||
}
|
||
|
||
options.forEach(optionText => {
|
||
const optionItem = document.createElement('li');
|
||
optionItem.className =
|
||
'pl-2 py-1 border-l-2 border-primary/30 ml-1 hover:border-primary/70 transition-colors text-gray-300 theme-transition';
|
||
optionItem.textContent = optionText;
|
||
this.elements.optionsList.appendChild(optionItem);
|
||
});
|
||
}
|
||
|
||
// 创建空状态提示
|
||
createEmptyState(message) {
|
||
return `<div class="text-center py-4 text-gray-500 theme-transition">
|
||
<i class="fa fa-info-circle mr-1"></i>${message}
|
||
</div>`;
|
||
}
|
||
|
||
// 设置事件监听器
|
||
setupEventListeners() {
|
||
const detailsElement = document.querySelector('details');
|
||
const contentElement = this.elements.charactersContainer;
|
||
|
||
// 初始化高度为0以实现动画效果
|
||
contentElement.style.maxHeight = '0';
|
||
|
||
// 监听详情展开/折叠事件
|
||
detailsElement.addEventListener('toggle', () => {
|
||
if (detailsElement.open) {
|
||
// 展开时设置实际高度
|
||
setTimeout(() => {
|
||
contentElement.style.maxHeight = contentElement.scrollHeight + 'px';
|
||
}, 10);
|
||
} else {
|
||
// 折叠时设置高度为0
|
||
contentElement.style.maxHeight = '0';
|
||
}
|
||
});
|
||
|
||
// 根据自动折叠设置决定默认状态
|
||
const autoCollapseToggle = document.getElementById('auto-collapse-toggle');
|
||
if (autoCollapseToggle) {
|
||
// 从本地存储读取设置,默认为true(折叠)
|
||
const savedAutoCollapse = localStorage.getItem('autoCollapse');
|
||
const shouldCollapse = savedAutoCollapse === null ? true : savedAutoCollapse === 'true';
|
||
detailsElement.open = !shouldCollapse;
|
||
|
||
// 如果默认展开,需要设置正确的高度
|
||
if (!shouldCollapse) {
|
||
setTimeout(() => {
|
||
contentElement.style.maxHeight = contentElement.scrollHeight + 'px';
|
||
}, 100);
|
||
}
|
||
} else {
|
||
// 如果没有设置切换开关,默认折叠
|
||
detailsElement.open = false;
|
||
}
|
||
}
|
||
|
||
// 错误处理
|
||
handleError(error) {
|
||
console.error('渲染错误:', error);
|
||
|
||
// 使用美化的错误弹窗
|
||
showErrorModal(error.message);
|
||
|
||
// 在角色状态区域显示错误信息
|
||
this.elements.charactersContainer.innerHTML = `
|
||
<div class="bg-red-900/20 border border-red-800/30 text-red-400 px-4 py-3 rounded relative" role="alert">
|
||
<strong class="font-bold">状态栏渲染失败: </strong>
|
||
<span class="block sm:inline">${error.message}</span>
|
||
</div>
|
||
`;
|
||
|
||
// 在行动选项区域也显示错误信息
|
||
this.elements.optionsList.innerHTML = `
|
||
<li class="text-red-400 bg-red-900/20 border border-red-800/30 rounded p-3 theme-transition">
|
||
<div class="flex items-start">
|
||
<i class="fa fa-exclamation-triangle mr-2 mt-1"></i>
|
||
<div>
|
||
<div class="font-semibold mb-1">行动选项加载失败</div>
|
||
<div class="text-sm text-red-300">请检查YAML格式是否正确</div>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
`;
|
||
}
|
||
}
|
||
|
||
$(document).ready(function () {
|
||
/**
|
||
* 获取本楼消息
|
||
* @returns {Object|null} 包含本楼消息信息的对象,失败时返回null
|
||
*/
|
||
function getCurrentMessage() {
|
||
try {
|
||
if (typeof getCurrentMessageId !== 'function' || typeof getChatMessages !== 'function') {
|
||
return null;
|
||
}
|
||
|
||
const currentMessageId = getCurrentMessageId();
|
||
if (!currentMessageId && currentMessageId !== 0) return null;
|
||
|
||
const messageData = getChatMessages(currentMessageId);
|
||
if (!messageData) return null;
|
||
|
||
return Array.isArray(messageData) && messageData.length > 0 ? messageData[0] : messageData;
|
||
} catch (error) {
|
||
console.error('获取消息失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function extractMaintext(message) {
|
||
if (!message || typeof message !== 'string') return '';
|
||
const match = message.match(/<maintext>([\s\S]*?)<\/maintext>/i);
|
||
return match ? match[1].trim() : '';
|
||
}
|
||
|
||
/**
|
||
* 从消息中提取Status_block内容
|
||
* @param {string} message 消息文本
|
||
* @returns {string} 提取的YAML状态内容
|
||
*/
|
||
function extractStatusBlock(message) {
|
||
if (!message || typeof message !== 'string') return '';
|
||
|
||
const match = message.match(/<Status_block>\s*([\s\S]*?)\s*<\/Status_block>/i);
|
||
return match ? cleanYamlContent(match[1].trim()) : '';
|
||
}
|
||
|
||
/**
|
||
* 清理YAML内容,修复常见的格式问题
|
||
* @param {string} yamlContent 原始YAML内容
|
||
* @returns {string} 清理后的YAML内容
|
||
*/
|
||
function cleanYamlContent(yamlContent) {
|
||
if (!yamlContent) return '';
|
||
|
||
return yamlContent
|
||
.split('\n')
|
||
.map(line => {
|
||
if (line.trim() === '' || !line.trim().match(/^-\s*"/)) return line;
|
||
|
||
const match = line.match(/^(\s*-\s*)"(.*)"\s*$/);
|
||
if (!match) return line;
|
||
|
||
const [, indent, content] = match;
|
||
return content.includes('"') || content.includes("'")
|
||
? indent + "'" + content.replace(/'/g, "''") + "'"
|
||
: indent + '"' + content + '"';
|
||
})
|
||
.join('\n');
|
||
}
|
||
|
||
/**
|
||
* 更新YAML数据源
|
||
* @param {string} yamlContent YAML格式的状态内容
|
||
*/
|
||
function updateYamlDataSource(yamlContent) {
|
||
const yamlScript = document.getElementById('yaml-data-source');
|
||
if (!yamlScript) return;
|
||
|
||
// 如果内容为空或无效,设置为加载中状态
|
||
if (!yamlContent || typeof yamlContent !== 'string' || !yamlContent.trim()) {
|
||
yamlScript.textContent = ''; // 设置为空,让后续处理显示加载状态
|
||
return;
|
||
}
|
||
|
||
// 先设置内容,让StoryRenderer能处理格式错误
|
||
yamlScript.textContent = yamlContent;
|
||
|
||
// 验证YAML格式,如果有错误会被StoryRenderer捕获并处理
|
||
try {
|
||
jsyaml.load(yamlContent);
|
||
} catch (error) {
|
||
// 尝试修复常见的YAML错误
|
||
const fixedYaml = attemptYamlFix(yamlContent, error);
|
||
if (fixedYaml) {
|
||
try {
|
||
jsyaml.load(fixedYaml);
|
||
yamlScript.textContent = fixedYaml;
|
||
} catch (e) {
|
||
console.error('YAML修复失败:', e.message);
|
||
// 修复失败时保留原内容,让StoryRenderer显示具体错误
|
||
}
|
||
}
|
||
// 如果无法修复,保留原内容,让StoryRenderer显示具体错误
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 尝试修复常见的YAML错误
|
||
* @param {string} yamlContent 有问题的YAML内容
|
||
* @param {Error} error YAML解析错误
|
||
* @returns {string|null} 修复后的YAML或null
|
||
*/
|
||
function attemptYamlFix(yamlContent, error) {
|
||
if (!(error.message.includes('bad indentation') || error.message.includes('quote'))) {
|
||
return null;
|
||
}
|
||
|
||
return yamlContent
|
||
.split('\n')
|
||
.map(line => {
|
||
const match = line.match(/^(\s*-\s*)"(.*)"\s*$/);
|
||
if (!match) return line;
|
||
|
||
const [, indent, content] = match;
|
||
return content.includes('"')
|
||
? indent + "'" + content.replace(/'/g, "''") + "'"
|
||
: indent + '"' + content + '"';
|
||
})
|
||
.join('\n');
|
||
}
|
||
|
||
/**
|
||
* 更新maintext内容
|
||
* @param {string} maintextContent maintext内容
|
||
*/
|
||
function updateMaintext(maintextContent) {
|
||
try {
|
||
const maintextElement = document.getElementById('maintext');
|
||
if (!maintextElement) return;
|
||
|
||
// 如果内容为空或无效,设置为加载中状态
|
||
if (!maintextContent || typeof maintextContent !== 'string' || !maintextContent.trim()) {
|
||
maintextElement.textContent = '';
|
||
} else {
|
||
maintextElement.textContent = maintextContent;
|
||
}
|
||
|
||
formatMainText();
|
||
} catch (error) {
|
||
console.error('更新maintext失败:', error);
|
||
// 如果更新失败,直接调用formatMainText,它会处理错误
|
||
formatMainText();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重新渲染状态栏
|
||
*/
|
||
function reRenderStatusBar() {
|
||
try {
|
||
const yamlScript = document.getElementById('yaml-data-source');
|
||
if (!yamlScript || !yamlScript.textContent) return;
|
||
|
||
const storyRenderer = new StoryRenderer('yaml-data-source');
|
||
storyRenderer.init();
|
||
} catch (error) {
|
||
console.error('重新渲染状态栏失败:', error);
|
||
// 状态栏渲染失败时,错误处理由StoryRenderer.handleError处理
|
||
// 这里不需要额外处理,因为StoryRenderer的init方法已经有handleError调用
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据消息数据渲染整个页面
|
||
* @param {Object} messageData 消息数据对象(格式参考test.json)
|
||
*/
|
||
function renderPageFromMessage(messageData) {
|
||
let actualMessageData = Array.isArray(messageData) && messageData.length > 0 ? messageData[0] : messageData;
|
||
|
||
if (!actualMessageData || !actualMessageData.message || typeof actualMessageData.message !== 'string') {
|
||
return;
|
||
}
|
||
|
||
const messageContent = actualMessageData.message;
|
||
|
||
// 提取并更新maintext内容
|
||
const maintextContent = extractMaintext(messageContent);
|
||
if (maintextContent) {
|
||
updateMaintext(maintextContent);
|
||
}
|
||
|
||
// 提取并更新Status_block内容
|
||
const statusContent = extractStatusBlock(messageContent);
|
||
if (statusContent) {
|
||
updateYamlDataSource(statusContent);
|
||
setTimeout(() => reRenderStatusBar(), 100);
|
||
}
|
||
}
|
||
|
||
// 执行获取操作并处理结果
|
||
try {
|
||
const currentMessage = getCurrentMessage();
|
||
if (currentMessage && typeof currentMessage === 'object') {
|
||
renderPageFromMessage(currentMessage);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取或渲染消息时出错:', error);
|
||
}
|
||
|
||
window.statusBlockInitialized = true;
|
||
});
|
||
</script>
|
||
|
||
</body></html> |