diff --git a/public/css/style.css b/public/css/style.css
index 655c937..ad8d60c 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -259,6 +259,7 @@ cap {
transition: opacity 0.5s;
}
+
/*
Class applied to all choices
(Will always appear inside
element by default.)
@@ -579,6 +580,13 @@ ol.choice {
width: 100%;
position: relative;
margin-top: 10px;
+ transition: opacity 0.5s ease; /* Add transition for fading effect */
+}
+
+/* Fade out command input when loading */
+#command_input.fading {
+ opacity: 0.3;
+ pointer-events: none; /* Prevent interaction while faded out */
}
/* Input wrapper for positioning cursor */
@@ -586,9 +594,12 @@ ol.choice {
position: relative;
width: 100%;
display: inline-block;
+ min-height: 1.5em; /* Minimum height for one line */
+ max-height: 6em; /* Maximum height - about 4 lines */
+ overflow: visible; /* Changed from 'auto' to 'visible' to hide scrollbars when not needed */
}
-/* Player input styling */
+/* Player input styling - now a textarea for multiline support */
#player_input {
width: 100%;
background: transparent;
@@ -601,10 +612,11 @@ ol.choice {
padding: 5px 0;
caret-color: transparent; /* Hide the default caret */
box-sizing: border-box;
-}
-
-#player_input:focus {
- border-bottom-color: #333;
+ resize: none; /* Disable manual resizing */
+ overflow: hidden; /* Hide all scrollbars */
+ line-height: 1.2; /* Match paragraph line height */
+ height: auto; /* Allow height to adjust */
+ min-height: 1.5em; /* Ensure minimum height */
}
/* Custom cursor styling */
@@ -618,12 +630,14 @@ ol.choice {
left: 0;
animation: blink 1s step-end infinite;
pointer-events: none; /* Allow clicks to pass through to the input */
+ z-index: 1; /* Ensure cursor appears above text */
}
-/* Placeholder styling - lighter and italic */
+/* Placeholder styling - lighter and italic, with padding to avoid cursor overlap */
#player_input::placeholder {
color: #aaa;
font-style: italic;
+ padding-left: 15px; /* Add padding to move placeholder text to the right */
}
/* Blinking animation */
@@ -631,3 +645,13 @@ ol.choice {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
+
+/* Fade-in animation for input area */
+@keyframes fadeInInput {
+ from { opacity: 0.3; }
+ to { opacity: 1; }
+}
+
+.fade-in-input {
+ animation: fadeInInput 0.5s ease forwards;
+}
diff --git a/public/index.html b/public/index.html
index 4a1c138..cfac093 100644
--- a/public/index.html
+++ b/public/index.html
@@ -34,7 +34,7 @@
diff --git a/public/js/ai-fiction.js b/public/js/ai-fiction.js
index 28a00af..3b02605 100644
--- a/public/js/ai-fiction.js
+++ b/public/js/ai-fiction.js
@@ -107,23 +107,159 @@ class AIFiction {
});
}
+ /**
+ * Helper function to get precise caret coordinates in a textarea
+ * @param {HTMLTextAreaElement} element - The textarea element
+ * @param {number} position - The caret position
+ * @return {Object} Object with top and left coordinates
+ */
+ getCaretCoordinates(element, position) {
+ // Create a range to represent the caret
+ const range = document.createRange();
+ const textNode = document.createTextNode(element.value.substring(0, position));
+ const span = document.createElement('span');
+ span.appendChild(textNode);
+
+ // Create a temporary div
+ const div = document.createElement('div');
+ div.style.position = 'absolute';
+ div.style.top = '-9999px';
+ div.style.left = '-9999px';
+ div.style.width = element.offsetWidth + 'px';
+ div.style.whiteSpace = 'pre-wrap';
+ div.style.wordWrap = 'break-word';
+ div.style.fontFamily = window.getComputedStyle(element).fontFamily;
+ div.style.fontSize = window.getComputedStyle(element).fontSize;
+ div.style.lineHeight = window.getComputedStyle(element).lineHeight;
+ div.style.padding = window.getComputedStyle(element).padding;
+
+ // Append everything to the DOM
+ div.appendChild(span);
+ document.body.appendChild(div);
+
+ // Measure the position
+ const coordinates = {
+ top: span.offsetTop,
+ left: span.offsetWidth
+ };
+
+ // Clean up
+ document.body.removeChild(div);
+
+ return coordinates;
+ }
+
+ /**
+ * Update the custom cursor position based on input text and caret position
+ * Enhanced version with simpler, more reliable positioning
+ */
+ updateCursorPosition() {
+ const input = this.playerInput;
+ const cursor = document.getElementById('cursor');
+
+ if (!cursor || !input) 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';
+ cursor.style.top = '6px'; // Default top position
+ return;
+ }
+
+ // Auto-adjust textarea height based on content
+ this.adjustTextareaHeight();
+
+ // Use a more reliable method to get cursor position:
+ // Create a temporary element that exactly duplicates the input's content and styling
+ const div = document.createElement('div');
+ div.style.position = 'absolute';
+ div.style.top = '-9999px';
+ div.style.left = '-9999px';
+ div.style.width = getComputedStyle(input).width;
+ div.style.height = 'auto';
+ div.style.padding = getComputedStyle(input).padding;
+ div.style.border = getComputedStyle(input).border;
+ div.style.fontFamily = getComputedStyle(input).fontFamily;
+ div.style.fontSize = getComputedStyle(input).fontSize;
+ div.style.fontWeight = getComputedStyle(input).fontWeight;
+ div.style.lineHeight = getComputedStyle(input).lineHeight;
+ div.style.whiteSpace = 'pre-wrap';
+ div.style.wordWrap = 'break-word';
+ div.style.boxSizing = getComputedStyle(input).boxSizing;
+
+ // Create three spans to help us position the cursor
+ const preCaretText = document.createElement('span');
+ preCaretText.textContent = inputText.substring(0, caretPosition);
+
+ const caretChar = document.createElement('span');
+ caretChar.textContent = '|'; // Visible cursor marker for measurement
+ caretChar.style.display = 'inline-block';
+ caretChar.style.width = '0';
+
+ const postCaretText = document.createElement('span');
+ postCaretText.textContent = inputText.substring(caretPosition);
+
+ // Add all elements to the DOM
+ div.appendChild(preCaretText);
+ div.appendChild(caretChar);
+ div.appendChild(postCaretText);
+ document.body.appendChild(div);
+
+ // Get position of the caret marker
+ const caretRect = caretChar.getBoundingClientRect();
+ const inputRect = input.getBoundingClientRect();
+
+ // Set cursor position
+ // We need to account for the input's scroll position
+ cursor.style.left = (caretRect.left - div.getBoundingClientRect().left) + 'px';
+ cursor.style.top = (caretRect.top - div.getBoundingClientRect().top - input.scrollTop) + 'px';
+
+ // Clean up
+ document.body.removeChild(div);
+ }
+
+ /**
+ * Adjust textarea height based on its content
+ */
+ adjustTextareaHeight() {
+ const textarea = this.playerInput;
+ if (!textarea) return;
+
+ // Reset height to auto to get the correct scrollHeight
+ textarea.style.height = 'auto';
+
+ // Set height to scrollHeight to fit content
+ textarea.style.height = textarea.scrollHeight + 'px';
+ }
+
/**
* Bind event handlers to DOM elements
*/
bindEvents() {
- // Submit command on Enter key
+ // Submit command on Enter key without Shift
this.playerInput.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault(); // Prevent default to avoid newline
this.submitCommand();
+ } else if (e.key === 'Enter' && e.shiftKey) {
+ // Allow Shift+Enter to create a new line
+ // Default behavior happens, no need to do anything
}
});
- // Handle cursor position based on input changes and caret position
- this.playerInput.addEventListener('input', this.updateCursorPosition.bind(this));
+ // Auto-resize textarea on input
+ this.playerInput.addEventListener('input', () => {
+ this.adjustTextareaHeight();
+ this.updateCursorPosition();
+ });
+
+ // Update cursor on various events
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();
@@ -133,6 +269,21 @@ class AIFiction {
document.getElementById('cursor').style.opacity = '0';
});
+ // Handle paste events
+ this.playerInput.addEventListener('paste', () => {
+ // Use setTimeout to let the paste complete before adjusting
+ setTimeout(() => {
+ this.adjustTextareaHeight();
+ this.updateCursorPosition();
+ }, 10);
+ });
+
+ // Handle window resize
+ window.addEventListener('resize', () => {
+ this.adjustTextareaHeight();
+ this.updateCursorPosition();
+ });
+
// Toggle speech
this.speechButton.addEventListener('click', () => {
// Check if the handler is available (it should be if button is enabled)
@@ -305,6 +456,10 @@ class AIFiction {
if (command === '') return;
+ // Fade out the input field
+ const commandInput = document.getElementById('command_input');
+ commandInput.classList.add('fading');
+
// Disable input temporarily
this.playerInput.disabled = true;
@@ -324,11 +479,25 @@ class AIFiction {
const cursor = document.getElementById('cursor');
if (cursor) {
cursor.style.left = '0px';
+ cursor.style.top = '6px';
}
+ // Reset textarea height
+ this.adjustTextareaHeight();
+
// Re-enable input field after a short delay (or after 8 seconds as failsafe)
const timeout = setTimeout(() => {
+ // Remove fading class and add fade-in animation
+ commandInput.classList.remove('fading');
+ commandInput.classList.add('fade-in-input');
+
+ // Remove animation class after it completes
+ setTimeout(() => {
+ commandInput.classList.remove('fade-in-input');
+ }, 500);
+
this.playerInput.disabled = false;
+ this.playerInput.focus();
// Remove thinking indicator
const thinkingElement = document.getElementById(thinkingId);
@@ -516,49 +685,21 @@ class AIFiction {
}
}
- /**
- * 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', () => {
+ // Force immediate focus on load
this.playerInput.focus();
+
+ // Some browsers might need a slight delay
+ setTimeout(() => this.playerInput.focus(), 100);
+
+ // Also adjust textarea height and update cursor position
+ this.adjustTextareaHeight();
+ this.updateCursorPosition();
});
// Focus input when user clicks anywhere in the document