590
docs/html/story_renderer_extract.js
Normal file
590
docs/html/story_renderer_extract.js
Normal file
@@ -0,0 +1,590 @@
|
||||
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>
|
||||
|
||||
|
||||
|
||||
|
||||
pm
|
||||
</body></html>
|
||||
Reference in New Issue
Block a user