Files
st-react/docs/html/my.html
2026-03-13 21:51:42 +08:00

1222 lines
52 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>渲染正文失败,请检查正文是否被 &lt;maintext&gt;正文&lt;/maintext&gt;包裹.</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>