Files
st-react/docs/html/story_renderer_extract.js
2026-03-03 04:28:33 +08:00

590 lines
23 KiB
JavaScript
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.

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>