Checkpoint current UI and ink integration state
This commit is contained in:
@@ -52,6 +52,7 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
this.notificationActive = false;
|
||||
this.pendingTerminalNotifications = [];
|
||||
this.latestInputMode = 'text';
|
||||
this.markdownRendererPromise = null;
|
||||
|
||||
// Resources to preload
|
||||
this.cssPath = '/css/style.css';
|
||||
@@ -130,9 +131,14 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
'openCreditsDialog',
|
||||
'closeCreditsDialog',
|
||||
'loadCreditsText',
|
||||
'getMarkdownRenderer',
|
||||
'renderMarkdown',
|
||||
'populateCreativeCredits',
|
||||
'creditLink',
|
||||
'createNotificationDialog',
|
||||
'handleStoryTag',
|
||||
'getTagMessage',
|
||||
'dispatchDeferredTagsForBlock',
|
||||
'showNotification',
|
||||
'displayNextNotification',
|
||||
'queueTerminalNotification',
|
||||
@@ -379,7 +385,7 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
controls.innerHTML = `
|
||||
<a id="speech"></a>
|
||||
<a id="autoplay"></a>
|
||||
<span><a id="speed_reset"><span id="speed_label"></span><sup>*</sup></a><input type="range" min="50" max="150" value="100" id="speed" name="speed" /></span>
|
||||
<span><a id="speed_reset"><span id="speed_label"></span></a><input type="range" min="50" max="150" value="100" id="speed" name="speed" /></span>
|
||||
<a id="rewind"></a>
|
||||
<a id="save"></a>
|
||||
<a id="reload" disabled="disabled"></a>
|
||||
@@ -391,6 +397,10 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
const choicesContainer = document.createElement('div');
|
||||
choicesContainer.id = 'choices';
|
||||
choicesContainer.className = 'container';
|
||||
|
||||
const controlSeparator = document.createElement('div');
|
||||
controlSeparator.id = 'left_control_separator';
|
||||
choicesContainer.appendChild(controlSeparator);
|
||||
|
||||
// Create command history container
|
||||
const commandHistory = document.createElement('div');
|
||||
@@ -414,13 +424,20 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
const remark = document.createElement('div');
|
||||
remark.id = 'remark';
|
||||
remark.innerHTML = `
|
||||
<div id="remark_hint"><i><sup>*</sup><span id="remark_text"></span></i></div>
|
||||
<div id="remark_hint"><i><span id="remark_text"></span></i></div>
|
||||
<div id="game_legal"></div>
|
||||
`;
|
||||
this.pageLeft.appendChild(remark);
|
||||
|
||||
bookContainer.appendChild(this.pageLeft);
|
||||
}
|
||||
|
||||
const choicesPanel = document.getElementById('choices');
|
||||
if (choicesPanel && !document.getElementById('left_control_separator')) {
|
||||
const controlSeparator = document.createElement('div');
|
||||
controlSeparator.id = 'left_control_separator';
|
||||
choicesPanel.insertBefore(controlSeparator, choicesPanel.firstChild);
|
||||
}
|
||||
|
||||
// Create or find page_right
|
||||
this.pageRight = document.getElementById('page_right');
|
||||
@@ -574,7 +591,7 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
setText('remark_text', 'title.fastForwardHint');
|
||||
setText('start_prompt', 'title.startPrompt');
|
||||
setText('credits_dialog_title', 'credits.title');
|
||||
setText('credits_close', 'credits.close');
|
||||
setText('credits_close_footer', 'credits.close');
|
||||
setText('story_popup_ok', 'popup.ok');
|
||||
setTitle('speech', 'topbar.speechTitle');
|
||||
setTitle('autoplay', 'topbar.autoplayTitle');
|
||||
@@ -601,16 +618,13 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
<div class="credits-dialog" role="dialog" aria-modal="true" aria-labelledby="credits_dialog_title">
|
||||
<div class="credits-dialog-header">
|
||||
<h2 id="credits_dialog_title"></h2>
|
||||
<button type="button" id="credits_close"></button>
|
||||
<button type="button" id="credits_close" class="close" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="credits-logo-row" aria-label="Credits links">
|
||||
<a href="https://openai.com/" target="_blank" rel="noreferrer"><img src="https://cdn.simpleicons.org/openai/2b2218" alt="OpenAI"></a>
|
||||
<a href="https://www.inklestudios.com/ink/" target="_blank" rel="noreferrer" class="credits-wordmark">ink</a>
|
||||
<a href="https://mnater.github.io/Hyphenopoly/" target="_blank" rel="noreferrer" class="credits-wordmark">Hyphenopoly</a>
|
||||
<a href="https://github.com/hexgrad/kokoro" target="_blank" rel="noreferrer" class="credits-wordmark">Kokoro</a>
|
||||
<a href="https://suno.com/" target="_blank" rel="noreferrer" class="credits-wordmark">Suno</a>
|
||||
<div id="credits_creative" class="credits-creative"></div>
|
||||
<div id="credits_content" class="credits-content"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="credits_close_footer"></button>
|
||||
</div>
|
||||
<pre id="credits_content" class="credits-content"></pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -622,10 +636,9 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
}
|
||||
});
|
||||
|
||||
const closeButton = document.getElementById('credits_close');
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', () => this.closeCreditsDialog());
|
||||
}
|
||||
[document.getElementById('credits_close'), document.getElementById('credits_close_footer')]
|
||||
.filter(Boolean)
|
||||
.forEach(button => button.addEventListener('click', () => this.closeCreditsDialog()));
|
||||
}
|
||||
|
||||
async openCreditsDialog() {
|
||||
@@ -640,9 +653,10 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
|
||||
if (!content.dataset.loaded) {
|
||||
content.textContent = this.t('credits.loading');
|
||||
content.textContent = await this.loadCreditsText();
|
||||
content.innerHTML = await this.renderMarkdown(await this.loadCreditsText());
|
||||
content.dataset.loaded = 'true';
|
||||
}
|
||||
this.populateCreativeCredits();
|
||||
}
|
||||
|
||||
closeCreditsDialog() {
|
||||
@@ -667,6 +681,86 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
}
|
||||
}
|
||||
|
||||
async getMarkdownRenderer() {
|
||||
if (!this.markdownRendererPromise) {
|
||||
this.markdownRendererPromise = import('/js/vendor/marked.esm.js')
|
||||
.then(module => module.marked || module.default || module);
|
||||
}
|
||||
return this.markdownRendererPromise;
|
||||
}
|
||||
|
||||
async renderMarkdown(markdown) {
|
||||
try {
|
||||
const renderer = await this.getMarkdownRenderer();
|
||||
if (typeof renderer.parse === 'function') {
|
||||
return renderer.parse(String(markdown || ''), { async: false });
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('UIDisplayHandler: Failed to render Markdown notices', error);
|
||||
}
|
||||
return `<pre>${String(markdown || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')}</pre>`;
|
||||
}
|
||||
|
||||
populateCreativeCredits() {
|
||||
const container = document.getElementById('credits_creative');
|
||||
if (!container || container.dataset.loaded) return;
|
||||
|
||||
const sections = [
|
||||
{
|
||||
title: 'Production',
|
||||
rows: [
|
||||
['Produced by', ['Bad Tools Studio']],
|
||||
['Story', ['Georg Tomitsch']],
|
||||
['Writing', ['Georg Tomitsch', 'ChatGPT']],
|
||||
['UI visual design', ['Georg Tomitsch']],
|
||||
['Typography', ['EB Garamond 12 by Georg Duffner and Octavio Pardo', 'EB Garamond Initials by Georg Duffner']],
|
||||
['Art direction', ['Georg Tomitsch']],
|
||||
['Music', ['Georg Tomitsch', 'Suno']],
|
||||
['Images', ['OpenAI GPT-image-2']]
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Technology',
|
||||
rows: [
|
||||
['Runtime server programming', ['Georg Tomitsch', 'OpenAI Codex']],
|
||||
['Client and UI programming', ['Georg Tomitsch', 'OpenAI Codex', 'Claude Code']],
|
||||
['Game engine', ['Ink by Inkle', 'inkjs by Yannick Lohse']]
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
container.innerHTML = sections.map(section => `
|
||||
<section class="credits-creative-column">
|
||||
<h3>${section.title}</h3>
|
||||
${section.rows.map(([label, names]) => `
|
||||
<div class="credits-creative-row">
|
||||
<dt>${label}</dt>
|
||||
<dd>${names.map(name => this.creditLink(name)).join(', ')}</dd>
|
||||
</div>
|
||||
`).join('')}
|
||||
</section>
|
||||
`).join('');
|
||||
container.dataset.loaded = 'true';
|
||||
}
|
||||
|
||||
creditLink(name) {
|
||||
const links = {
|
||||
'Bad Tools Studio': '',
|
||||
'OpenAI Codex': 'https://openai.com/codex/',
|
||||
'OpenAI GPT-image-2': 'https://openai.com/',
|
||||
'ChatGPT': 'https://chatgpt.com/',
|
||||
'Claude Code': 'https://www.anthropic.com/claude-code',
|
||||
'Ink by Inkle': 'https://www.inklestudios.com/ink/',
|
||||
'inkjs by Yannick Lohse': 'https://www.npmjs.com/package/inkjs',
|
||||
'EB Garamond 12 by Georg Duffner and Octavio Pardo': 'https://github.com/octaviopardo/EBGaramond12',
|
||||
'EB Garamond Initials by Georg Duffner': 'https://github.com/georgd/EB-Garamond',
|
||||
'Suno': 'https://suno.com/'
|
||||
};
|
||||
const escaped = String(name || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const url = links[name];
|
||||
return url ? `<a href="${url}" target="_blank" rel="noreferrer">${escaped}</a>` : escaped;
|
||||
}
|
||||
|
||||
createNotificationDialog() {
|
||||
if (document.getElementById('story_popup_modal')) {
|
||||
return;
|
||||
@@ -678,9 +772,14 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
modal.innerHTML = `
|
||||
<div class="story-popup-dialog" role="dialog" aria-modal="true" aria-labelledby="story_popup_title">
|
||||
<h2 id="story_popup_title"></h2>
|
||||
<div class="story-popup-dialog-header">
|
||||
<h2 id="story_popup_title"></h2>
|
||||
<button type="button" id="story_popup_close" class="close" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div id="story_popup_message"></div>
|
||||
<button type="button" id="story_popup_ok"></button>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="story_popup_ok"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -696,14 +795,13 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
const okButton = document.getElementById('story_popup_ok');
|
||||
if (okButton) {
|
||||
okButton.addEventListener('click', (event) => {
|
||||
[document.getElementById('story_popup_ok'), document.getElementById('story_popup_close')]
|
||||
.filter(Boolean)
|
||||
.forEach(button => button.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.closeNotification();
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
handleStoryTag(tag) {
|
||||
@@ -714,13 +812,13 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
|
||||
const message = this.getTagMessage(tag);
|
||||
if (key === 'score') {
|
||||
this.queueTerminalNotification(
|
||||
this.showNotification(
|
||||
'ending',
|
||||
this.t('popup.endingTitle'),
|
||||
message || this.t('popup.defaultEnding')
|
||||
);
|
||||
} else if (key === 'error') {
|
||||
this.queueTerminalNotification(
|
||||
this.showNotification(
|
||||
'error',
|
||||
this.t('popup.errorTitle'),
|
||||
message || this.t('popup.defaultError')
|
||||
@@ -747,6 +845,28 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
dispatchDeferredTagsForBlock(block) {
|
||||
const directTags = Array.isArray(block?.deferredTags) ? block.deferredTags : [];
|
||||
const metadataTags = Array.isArray(block?.metadata?.deferredTags) ? block.metadata.deferredTags : [];
|
||||
const tags = [...directTags, ...metadataTags];
|
||||
if (tags.length === 0) return;
|
||||
|
||||
tags.forEach((tag) => {
|
||||
if (!tag?.key) return;
|
||||
document.dispatchEvent(new CustomEvent('story:tag', {
|
||||
detail: {
|
||||
...tag,
|
||||
blockId: block.blockId ?? null,
|
||||
turnId: block.turnId ?? null
|
||||
}
|
||||
}));
|
||||
});
|
||||
block.deferredTags = [];
|
||||
if (block.metadata) {
|
||||
block.metadata.deferredTags = [];
|
||||
}
|
||||
}
|
||||
|
||||
showNotification(kind, title, message) {
|
||||
this.notificationQueue.push({ kind, title, message });
|
||||
this.displayNextNotification();
|
||||
@@ -852,6 +972,8 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
console.log('UIDisplayHandler: Music block started', sentence.metadata || {});
|
||||
}
|
||||
|
||||
this.dispatchDeferredTagsForBlock(sentence);
|
||||
|
||||
if (sentence.onComplete) {
|
||||
sentence.onComplete();
|
||||
}
|
||||
@@ -1344,6 +1466,16 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
blockId: item.blockId ?? item.metadata?.blockId,
|
||||
gameId: item.gameId ?? item.metadata?.gameId
|
||||
};
|
||||
if (metadata.dropCap && typeof sentenceQueue.measureDropCapReservation === 'function') {
|
||||
const dropCapText = typeof sentenceQueue.getDropCapText === 'function'
|
||||
? sentenceQueue.getDropCapText(metadata.layoutText || item.text || '')
|
||||
: String(metadata.layoutText || item.text || '').trim().charAt(0);
|
||||
metadata.dropCapWidth = sentenceQueue.measureDropCapReservation(
|
||||
this.container || this.paragraphContainer || document.getElementById('story'),
|
||||
dropCapText,
|
||||
this.measureStoryLineHeight()
|
||||
);
|
||||
}
|
||||
|
||||
const role = metadata.role;
|
||||
const isHeading = type === 'heading' || role === 'chapter-heading' || role === 'section-heading';
|
||||
@@ -1472,7 +1604,11 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
const lineHeight = this.measureStoryLineHeight();
|
||||
const isHeading = metadata.type === 'heading' || metadata.role === 'chapter-heading' || metadata.role === 'section-heading';
|
||||
const dropCapLines = metadata.dropCap ? 2 : 0;
|
||||
const dropCapWidth = metadata.dropCap ? lineHeight * 1.45 : 0;
|
||||
const dropCapWidth = metadata.dropCap
|
||||
? (Number.isFinite(Number(metadata.dropCapWidth)) && Number(metadata.dropCapWidth) > 0
|
||||
? Number(metadata.dropCapWidth)
|
||||
: lineHeight * 1.34)
|
||||
: 0;
|
||||
const indentWidth = (isHeading || metadata.isFirstParagraphInChapter || metadata.addTopSpace) ? 0 : lineHeight * 1.5;
|
||||
const maxConsideredLines = Math.max(80, this.pageLineCount * 4);
|
||||
const measures = [];
|
||||
@@ -1510,7 +1646,7 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
word.style.visibility = 'visible';
|
||||
word.style.opacity = '1';
|
||||
word.style.transform = 'translateY(0)';
|
||||
word.style.clipPath = 'inset(0 0 0 0)';
|
||||
word.style.clipPath = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2162,11 +2298,9 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
: maxOuterWidth;
|
||||
const naturalHeight = maxImageWidth / aspect;
|
||||
const imageLineCount = Math.max(1, Math.floor(naturalHeight / lineHeight));
|
||||
const verticalMargin = isPortrait ? lineHeight / 2 : 0;
|
||||
const lineCount = isPortrait ? imageLineCount + 1 : imageLineCount;
|
||||
const height = isPortrait
|
||||
? Math.max(lineHeight, (lineCount * lineHeight) - (verticalMargin * 2))
|
||||
: imageLineCount * lineHeight;
|
||||
const verticalMargin = lineHeight / 2;
|
||||
const lineCount = imageLineCount + 1;
|
||||
const height = Math.max(lineHeight, (lineCount * lineHeight) - (verticalMargin * 2));
|
||||
const width = Math.min(maxImageWidth, height * aspect);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user