diff --git a/public/css/style.css b/public/css/style.css index aa3b14b..655c937 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -259,20 +259,6 @@ cap { transition: opacity 0.5s; } -#choices { - display: grid; - grid-template-columns: repeat(3, 1fr); - width: calc(var(--book-width) * 0.39)px; -} - -#choices *:first-child { - grid-column: 1 / -1; -} - -#choices ol.categorized { - list-style-type: lower-alpha; -} - /* Class applied to all choices (Will always appear inside

element by default.) @@ -582,32 +568,66 @@ ol.choice { /* Input area */ #input_area { - display: flex; - margin-bottom: 15px; + display: block; + margin-top: 15px; + margin-bottom: 10px; + width: 100%; } +/* Command input container */ +#command_input { + width: 100%; + position: relative; + margin-top: 10px; +} + +/* Input wrapper for positioning cursor */ +.input-wrapper { + position: relative; + width: 100%; + display: inline-block; +} + +/* Player input styling */ #player_input { - flex: 1; - padding: 8px 12px; - border: 1px solid #d1c8b9; - background: rgba(255, 255, 255, 0.8); + width: 100%; + background: transparent; + border: none; + border-bottom: 1px solid #8b7765; font-family: 'EB Garamond', serif; - font-size: 16px; + font-size: 1.1rem; + color: #333; outline: none; - border-radius: 4px 0 0 4px; + padding: 5px 0; + caret-color: transparent; /* Hide the default caret */ + box-sizing: border-box; } -#submit_command { - background-color: #8b7765; - border: 1px solid #8b7765; - color: white; - padding: 8px 12px; - cursor: pointer; - font-family: 'EB Garamond', serif; - border-radius: 0 4px 4px 0; - transition: background-color 0.2s; +#player_input:focus { + border-bottom-color: #333; } -#submit_command:hover { - background-color: #6d5d4d; +/* Custom cursor styling */ +#cursor { + position: absolute; + display: inline-block; + width: 8px; + height: 1.2em; + background-color: #333; + top: 6px; + left: 0; + animation: blink 1s step-end infinite; + pointer-events: none; /* Allow clicks to pass through to the input */ +} + +/* Placeholder styling - lighter and italic */ +#player_input::placeholder { + color: #aaa; + font-style: italic; +} + +/* Blinking animation */ +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } } diff --git a/public/index.html b/public/index.html index d7bf8b5..4a1c138 100644 --- a/public/index.html +++ b/public/index.html @@ -33,8 +33,10 @@

- - +
+ + +
*click on page or press spacebar to fast forward text animation
diff --git a/public/js/ai-fiction.js b/public/js/ai-fiction.js index 0ccdc6c..28a00af 100644 --- a/public/js/ai-fiction.js +++ b/public/js/ai-fiction.js @@ -8,7 +8,6 @@ class AIFiction { this.storyContainer = document.getElementById('story'); this.commandHistoryContainer = document.getElementById('command_history'); this.playerInput = document.getElementById('player_input'); - this.submitButton = document.getElementById('submit_command'); this.speechButton = document.getElementById('speech'); this.rewindButton = document.getElementById('rewind'); this.saveButton = document.getElementById('save'); @@ -33,9 +32,6 @@ class AIFiction { this.typingSpeed = 30; // Default value, will be adjusted by slider this.typingTimeout = null; - // Check for kokoro-js being loaded (Now handled by factory) - // this.checkForKokoroJs(); // No longer needed here - // Bind event handlers this.bindEvents(); @@ -47,6 +43,9 @@ class AIFiction { // Listen for TTS readiness this.listenForTTSReady(); + + // Set up focus management + this.setupFocusManagement(); } /** @@ -112,9 +111,6 @@ class AIFiction { * Bind event handlers to DOM elements */ bindEvents() { - // Submit command on button click - this.submitButton.addEventListener('click', () => this.submitCommand()); - // Submit command on Enter key this.playerInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { @@ -122,6 +118,21 @@ class AIFiction { } }); + // Handle cursor position based on input changes and caret position + this.playerInput.addEventListener('input', this.updateCursorPosition.bind(this)); + this.playerInput.addEventListener('click', this.updateCursorPosition.bind(this)); + this.playerInput.addEventListener('keyup', this.updateCursorPosition.bind(this)); + + // Handle cursor visibility based on input focus + this.playerInput.addEventListener('focus', () => { + document.getElementById('cursor').style.opacity = '1'; + this.updateCursorPosition(); + }); + + this.playerInput.addEventListener('blur', () => { + document.getElementById('cursor').style.opacity = '0'; + }); + // Toggle speech this.speechButton.addEventListener('click', () => { // Check if the handler is available (it should be if button is enabled) @@ -133,7 +144,6 @@ class AIFiction { // Set user activation flag for the handler window.ttsHandler.hasUserActivation = true; - const enabled = window.ttsHandler.toggle(); this.updateSpeechButton(enabled); @@ -249,11 +259,10 @@ class AIFiction { // Scroll to bottom and focus input this.scrollToBottom(); - this.playerInput.focus(); + this.playerInput.focus(); // Re-enable input (failsafe) this.playerInput.disabled = false; - this.submitButton.disabled = false; }); // Game saved confirmation @@ -298,7 +307,6 @@ class AIFiction { // Disable input temporarily this.playerInput.disabled = true; - this.submitButton.disabled = true; // Add command to history this.addUserCommand(command); @@ -312,10 +320,15 @@ class AIFiction { // Clear input this.playerInput.value = ''; + // Reset cursor position to the start + const cursor = document.getElementById('cursor'); + if (cursor) { + cursor.style.left = '0px'; + } + // Re-enable input field after a short delay (or after 8 seconds as failsafe) const timeout = setTimeout(() => { this.playerInput.disabled = false; - this.submitButton.disabled = false; // Remove thinking indicator const thinkingElement = document.getElementById(thinkingId); @@ -502,6 +515,85 @@ class AIFiction { container.scrollTop = container.scrollHeight; } } + + /** + * Update the custom cursor position based on input text and caret position + */ + updateCursorPosition() { + const input = this.playerInput; + const cursor = document.getElementById('cursor'); + + if (!cursor) return; + + // Get the current caret position + const caretPosition = input.selectionStart || 0; + const inputText = input.value; + + if (inputText.length === 0) { + // If no text, position cursor at the beginning (placeholder visible) + cursor.style.left = '0px'; + return; + } + + // Create a temporary span to measure text width + const tempSpan = document.createElement('span'); + tempSpan.style.font = window.getComputedStyle(input).font; + tempSpan.style.position = 'absolute'; + tempSpan.style.visibility = 'hidden'; + tempSpan.style.whiteSpace = 'pre'; + tempSpan.textContent = inputText.substring(0, caretPosition); + document.body.appendChild(tempSpan); + + // Set cursor position based on the width of text before caret + const textWidth = tempSpan.getBoundingClientRect().width; + cursor.style.left = `${textWidth}px`; + + // Clean up + document.body.removeChild(tempSpan); + } + + /** + * Set up focus management to ensure input field is always focused + */ + setupFocusManagement() { + // Focus input field when the page loads + window.addEventListener('load', () => { + this.playerInput.focus(); + }); + + // Focus input when user clicks anywhere in the document + document.addEventListener('click', (e) => { + // Don't steal focus if user is clicking on a button or link + if ( + e.target.tagName !== 'BUTTON' && + e.target.tagName !== 'A' && + !e.target.classList.contains('suggestions') && + !e.target.closest('.suggestions') + ) { + this.playerInput.focus(); + } + }); + + // Re-focus input when user returns to this browser tab + window.addEventListener('focus', () => { + this.playerInput.focus(); + }); + + // Re-focus input when user returns to the window + window.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + setTimeout(() => this.playerInput.focus(), 100); + } + }); + + // Focus on input after narrative is added + const originalAddNarrative = this.addNarrative.bind(this); + this.addNarrative = (text) => { + originalAddNarrative(text); + // Short timeout to ensure rendering completes + setTimeout(() => this.playerInput.focus(), 10); + }; + } } // Create the application when the DOM is fully loaded