Checkpoint current UI and ink integration state

This commit is contained in:
2026-05-18 02:46:02 +02:00
parent 2c54498ee2
commit d7bb175167
384 changed files with 922883 additions and 764 deletions
+166 -32
View File
@@ -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">&times;</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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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">&times;</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 {