From ad1e7647f2cfc17e87d65efc6d2214b2d5553e9d Mon Sep 17 00:00:00 2001 From: Micro66 Date: Tue, 18 Nov 2025 17:03:37 +0800 Subject: [PATCH 1/5] Add markdown-like line formatting support to chat input - Implement line formatting that preserves indentation and spacing - Add auto-focus functionality for better user experience - Replace onChange with onInput for real-time formatting - Support maintaining leading whitespace when creating new lines - Improve text display with pre-wrap and word-wrap styling --- .../features/tasks/components/ChatInput.tsx | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/frontend/src/features/tasks/components/ChatInput.tsx b/frontend/src/features/tasks/components/ChatInput.tsx index b7bc4819..1f3c911f 100644 --- a/frontend/src/features/tasks/components/ChatInput.tsx +++ b/frontend/src/features/tasks/components/ChatInput.tsx @@ -4,7 +4,7 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; import { useTranslation } from '@/hooks/useTranslation'; import { useIsMobile } from '@/features/layout/hooks/useMediaQuery'; @@ -29,6 +29,7 @@ export default function ChatInput({ const placeholderKey = taskType === 'chat' ? 'chat.placeholder_chat' : 'chat.placeholder_code'; const [isComposing, setIsComposing] = useState(false); const isMobile = useIsMobile(); + const textareaRef = useRef(null); const handleCompositionStart = () => { setIsComposing(true); @@ -51,13 +52,52 @@ export default function ChatInput({ } }; + const handleInput = (e: React.FormEvent) => { + const textarea = e.target as HTMLTextAreaElement; + const value = textarea.value; + + // Handle line formatting similar to markdown + // When user presses Enter, maintain the indentation and formatting + if (value.includes('\n')) { + const lines = value.split('\n'); + const formattedLines = lines.map((line, index) => { + // For lines after the first one, preserve leading spaces/tabs + if (index > 0) { + // Count leading whitespace in the previous line + const prevLine = lines[index - 1]; + const leadingWhitespace = prevLine.match(/^\s*/)?.[0] || ''; + + // If current line is empty and previous line has content, preserve indentation + if (line.trim() === '' && prevLine.trim() !== '') { + return leadingWhitespace; + } + } + return line; + }); + + const formattedValue = formattedLines.join('\n'); + if (formattedValue !== value) { + setMessage(formattedValue); + return; + } + } + + setMessage(value); + }; + + // Auto-focus on mount + useEffect(() => { + if (textareaRef.current && !disabled) { + textareaRef.current.focus(); + } + }, [disabled]); + return (
{ - if (!disabled) setMessage(e.target.value); - }} + onInput={handleInput} onKeyDown={handleKeyPress} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} @@ -66,7 +106,7 @@ export default function ChatInput({ disabled={disabled} minRows={isMobile ? 2 : 3} maxRows={isMobile ? 6 : 8} - style={{ resize: 'none', overflow: 'auto' }} + style={{ resize: 'none', overflow: 'auto', whiteSpace: 'pre-wrap', wordWrap: 'break-word' }} />
); From 1fb7b24f1d8c614279dc7b7536ab336661c6d10f Mon Sep 17 00:00:00 2001 From: Micro66 Date: Tue, 18 Nov 2025 18:08:25 +0800 Subject: [PATCH 2/5] Fix input formatting and space input issues - Replace problematic onInput handler with proper onKeyDown approach - Implement auto-indentation only on Shift+Enter key combination - Preserve whitespace and normal input behavior for all other keys - Fix cursor positioning after auto-indentation - Maintain existing Enter key behavior for message sending --- .../features/tasks/components/ChatInput.tsx | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/frontend/src/features/tasks/components/ChatInput.tsx b/frontend/src/features/tasks/components/ChatInput.tsx index 1f3c911f..75b8a2b1 100644 --- a/frontend/src/features/tasks/components/ChatInput.tsx +++ b/frontend/src/features/tasks/components/ChatInput.tsx @@ -52,37 +52,32 @@ export default function ChatInput({ } }; - const handleInput = (e: React.FormEvent) => { - const textarea = e.target as HTMLTextAreaElement; - const value = textarea.value; - - // Handle line formatting similar to markdown - // When user presses Enter, maintain the indentation and formatting - if (value.includes('\n')) { - const lines = value.split('\n'); - const formattedLines = lines.map((line, index) => { - // For lines after the first one, preserve leading spaces/tabs - if (index > 0) { - // Count leading whitespace in the previous line - const prevLine = lines[index - 1]; - const leadingWhitespace = prevLine.match(/^\s*/)?.[0] || ''; - - // If current line is empty and previous line has content, preserve indentation - if (line.trim() === '' && prevLine.trim() !== '') { - return leadingWhitespace; - } - } - return line; - }); - - const formattedValue = formattedLines.join('\n'); - if (formattedValue !== value) { - setMessage(formattedValue); - return; - } - } + const handleKeyDown = (e: React.KeyboardEvent) => { + // Handle Enter key for auto-indentation (only when Shift+Enter) + if (e.key === 'Enter' && e.shiftKey && !disabled && !isComposing) { + e.preventDefault(); + + const textarea = e.target as HTMLTextAreaElement; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const value = textarea.value; + + // Get the current line and its leading whitespace + const lines = value.substring(0, start).split('\n'); + const currentLine = lines[lines.length - 1]; + const leadingWhitespace = currentLine.match(/^\s*/)?.[0] || ''; - setMessage(value); + // Insert new line with preserved indentation + const newValue = value.substring(0, start) + '\n' + leadingWhitespace + value.substring(end); + + setMessage(newValue); + + // Set cursor position after the inserted whitespace + setTimeout(() => { + textarea.selectionStart = start + 1 + leadingWhitespace.length; + textarea.selectionEnd = start + 1 + leadingWhitespace.length; + }, 0); + } }; // Auto-focus on mount @@ -97,8 +92,11 @@ export default function ChatInput({ { + if (!disabled) setMessage(e.target.value); + }} + onKeyDown={handleKeyDown} + onKeyPress={handleKeyPress} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} placeholder={t(placeholderKey)} From 721db183d0b6743f0227e1d5a1d8678313cd6635 Mon Sep 17 00:00:00 2001 From: Micro66 Date: Tue, 18 Nov 2025 18:13:28 +0800 Subject: [PATCH 3/5] Add Tab key support for inserting \t placeholder - Handle Tab key to insert \t placeholder instead of default focus switching - Prevent default Tab behavior and insert tab character at cursor position - Maintain proper cursor positioning after tab insertion - Preserve existing Shift+Enter auto-indentation functionality --- .../features/tasks/components/ChatInput.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/frontend/src/features/tasks/components/ChatInput.tsx b/frontend/src/features/tasks/components/ChatInput.tsx index 75b8a2b1..868d1b9a 100644 --- a/frontend/src/features/tasks/components/ChatInput.tsx +++ b/frontend/src/features/tasks/components/ChatInput.tsx @@ -53,6 +53,28 @@ export default function ChatInput({ }; const handleKeyDown = (e: React.KeyboardEvent) => { + // Handle Tab key - insert \t placeholder instead of default tab behavior + if (e.key === 'Tab' && !disabled && !isComposing) { + e.preventDefault(); + + const textarea = e.target as HTMLTextAreaElement; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const value = textarea.value; + + // Insert \t placeholder at cursor position + const newValue = value.substring(0, start) + '\t' + value.substring(end); + + setMessage(newValue); + + // Set cursor position after the inserted tab + setTimeout(() => { + textarea.selectionStart = start + 1; + textarea.selectionEnd = start + 1; + }, 0); + return; + } + // Handle Enter key for auto-indentation (only when Shift+Enter) if (e.key === 'Enter' && e.shiftKey && !disabled && !isComposing) { e.preventDefault(); From 8f6c58add4f58918c8a7d0524c01ef8018a63573 Mon Sep 17 00:00:00 2001 From: Micro66 Date: Tue, 18 Nov 2025 18:18:31 +0800 Subject: [PATCH 4/5] Add Backspace support for removing auto-indented whitespace - Handle Backspace key to remove auto-indented whitespace efficiently - Delete entire line of whitespace when cursor is at empty indented line - Remove all leading whitespace when Backspacing at line start - Maintain proper cursor positioning after deletion - Preserve existing Tab and Shift+Enter functionality --- .../features/tasks/components/ChatInput.tsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/frontend/src/features/tasks/components/ChatInput.tsx b/frontend/src/features/tasks/components/ChatInput.tsx index 868d1b9a..7ee971d9 100644 --- a/frontend/src/features/tasks/components/ChatInput.tsx +++ b/frontend/src/features/tasks/components/ChatInput.tsx @@ -75,6 +75,62 @@ export default function ChatInput({ return; } + // Handle Backspace key - remove auto-indented whitespace + if (e.key === 'Backspace' && !disabled && !isComposing) { + const textarea = e.target as HTMLTextAreaElement; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const value = textarea.value; + + // Only handle when cursor is at a single position (no selection) + if (start === end && start > 0) { + // Get the current line and previous character + const lines = value.substring(0, start).split('\n'); + const currentLine = lines[lines.length - 1]; + + // Check if we're at the beginning of a line and the line contains only whitespace + if (currentLine.trim() === '' && currentLine.length > 0) { + e.preventDefault(); + + // Remove the entire line of whitespace and the newline character + const previousLines = lines.slice(0, -1); + const remainingText = value.substring(start); + const newValue = previousLines.join('\n') + (previousLines.length > 0 ? '\n' : '') + remainingText; + + setMessage(newValue); + + // Set cursor position at the end of the previous line + const newCursorPos = previousLines.length > 0 ? previousLines.join('\n').length + 1 : 0; + setTimeout(() => { + textarea.selectionStart = newCursorPos; + textarea.selectionEnd = newCursorPos; + }, 0); + return; + } + + // Check if we're deleting whitespace that was auto-indented + const charBeforeCursor = value.substring(start - 1, start); + if (charBeforeCursor === ' ' || charBeforeCursor === '\t') { + // Get the line start position + const lineStart = value.lastIndexOf('\n', start - 1) + 1; + const lineContent = value.substring(lineStart, start); + + // If the line contains only whitespace up to cursor, remove all whitespace + if (lineContent.trim() === '') { + e.preventDefault(); + const newValue = value.substring(0, lineStart) + value.substring(start); + setMessage(newValue); + + setTimeout(() => { + textarea.selectionStart = lineStart; + textarea.selectionEnd = lineStart; + }, 0); + return; + } + } + } + } + // Handle Enter key for auto-indentation (only when Shift+Enter) if (e.key === 'Enter' && e.shiftKey && !disabled && !isComposing) { e.preventDefault(); From b112eda2505dcd7d9dad4cb8329a3c1f681bcde4 Mon Sep 17 00:00:00 2001 From: Micro66 Date: Tue, 18 Nov 2025 19:02:22 +0800 Subject: [PATCH 5/5] Add mobile support for input formatting features - Enable auto-indentation on mobile Enter key press - Add mobile Tab button for inserting \t placeholder - Differentiate Enter key behavior between desktop and mobile - Mobile: Enter creates new line with auto-indentation - Desktop: Enter sends message, Shift+Enter creates new line - Add mobile toolbar with Tab button for better UX --- .../features/tasks/components/ChatInput.tsx | 85 ++++++++++++++----- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/frontend/src/features/tasks/components/ChatInput.tsx b/frontend/src/features/tasks/components/ChatInput.tsx index 7ee971d9..bbfbc1c3 100644 --- a/frontend/src/features/tasks/components/ChatInput.tsx +++ b/frontend/src/features/tasks/components/ChatInput.tsx @@ -131,33 +131,68 @@ export default function ChatInput({ } } - // Handle Enter key for auto-indentation (only when Shift+Enter) - if (e.key === 'Enter' && e.shiftKey && !disabled && !isComposing) { - e.preventDefault(); + // Handle Enter key for auto-indentation + if (e.key === 'Enter' && !disabled && !isComposing) { + // Desktop: Enter sends message, Shift+Enter creates new line + if (!isMobile && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + return; + } - const textarea = e.target as HTMLTextAreaElement; - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - const value = textarea.value; + // Mobile: Enter creates new line with auto-indentation + // Desktop: Shift+Enter creates new line with auto-indentation + if (isMobile || e.shiftKey) { + e.preventDefault(); - // Get the current line and its leading whitespace - const lines = value.substring(0, start).split('\n'); - const currentLine = lines[lines.length - 1]; - const leadingWhitespace = currentLine.match(/^\s*/)?.[0] || ''; + const textarea = e.target as HTMLTextAreaElement; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const value = textarea.value; - // Insert new line with preserved indentation - const newValue = value.substring(0, start) + '\n' + leadingWhitespace + value.substring(end); + // Get the current line and its leading whitespace + const lines = value.substring(0, start).split('\n'); + const currentLine = lines[lines.length - 1]; + const leadingWhitespace = currentLine.match(/^\s*/)?.[0] || ''; - setMessage(newValue); + // Insert new line with preserved indentation + const newValue = value.substring(0, start) + '\n' + leadingWhitespace + value.substring(end); - // Set cursor position after the inserted whitespace - setTimeout(() => { - textarea.selectionStart = start + 1 + leadingWhitespace.length; - textarea.selectionEnd = start + 1 + leadingWhitespace.length; - }, 0); + setMessage(newValue); + + // Set cursor position after the inserted whitespace + setTimeout(() => { + textarea.selectionStart = start + 1 + leadingWhitespace.length; + textarea.selectionEnd = start + 1 + leadingWhitespace.length; + }, 0); + return; + } } }; + const handleInsertTab = () => { + if (disabled || isComposing) return; + + const textarea = textareaRef.current; + if (!textarea) return; + + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const value = textarea.value; + + // Insert \t placeholder at cursor position + const newValue = value.substring(0, start) + '\t' + value.substring(end); + + setMessage(newValue); + + // Set cursor position after inserted tab + setTimeout(() => { + textarea.selectionStart = start + 1; + textarea.selectionEnd = start + 1; + textarea.focus(); + }, 0); + }; + // Auto-focus on mount useEffect(() => { if (textareaRef.current && !disabled) { @@ -184,6 +219,18 @@ export default function ChatInput({ maxRows={isMobile ? 6 : 8} style={{ resize: 'none', overflow: 'auto', whiteSpace: 'pre-wrap', wordWrap: 'break-word' }} /> + {/* Mobile toolbar with Tab button */} + {isMobile && ( +
+ +
+ )} ); }