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 @@
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