Clean up: remove backup files, temp scripts, and debug files
Some checks are pending
Documentation Site Preview / deploy (push) Waiting to run

- Remove *.backup files (ChatInput.tsx.backup, RichChatInput.tsx.backup, Cargo.toml.backup)
- Remove temp_* files (temp_smart_spellcheck.js, temp_batch_issues.md, temp_test_typo.js)
- Remove fix_*.py scripts (fix_action_button.py, fix_action_insertion.py)
- Remove modify_*.js scripts (modify_rich_chat_input_tooltip_hover.js)
- Clean repository for production readiness
This commit is contained in:
spencrmartin 2025-09-25 10:45:04 -04:00
parent 80cf2f10ee
commit 30607faed6
8 changed files with 0 additions and 2694 deletions

View file

@ -1,21 +0,0 @@
[workspace]
members = ["crates/*"]
resolver = "2"
[workspace.package]
edition = "2021"
version = "1.9.0"
authors = ["Block <ai-oss-tools@block.xyz>"]
license = "Apache-2.0"
repository = "https://github.com/block/goose"
description = "An AI agent"
[workspace.lints.clippy]
uninlined_format_args = "allow"
[workspace.dependencies]
rmcp = { version = "0.6.2", features = ["schemars", "auth"] }
# Patch for Windows cross-compilation issue with crunchy
[patch.crates-io]
crunchy = { git = "https://github.com/nmathewson/crunchy", branch = "cross-compilation-fix" }

View file

@ -1,46 +0,0 @@
import re
# Read the ChatInput.tsx file
with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f:
content = f.read()
# Find and replace the handleActionButtonClick function
old_function = ''' const handleActionButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
const buttonRect = event.currentTarget.getBoundingClientRect();
setActionPopover({
isOpen: true,
position: {
x: buttonRect.left,
y: buttonRect.top,
},
selectedIndex: 0,
cursorPosition: textAreaRef.current?.getBoundingClientRect ? 0 : 0, // Will be set by RichChatInput
});
};'''
new_function = ''' const handleActionButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
const buttonRect = event.currentTarget.getBoundingClientRect();
// Get the current cursor position from the RichChatInput
const currentCursorPosition = textAreaRef.current?.getBoundingClientRect ? displayValue.length : 0;
setActionPopover({
isOpen: true,
position: {
x: buttonRect.left,
y: buttonRect.top,
},
selectedIndex: 0,
cursorPosition: currentCursorPosition,
});
};'''
# Replace the function
content = content.replace(old_function, new_function)
# Write back to file
with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f:
f.write(content)
print("Updated handleActionButtonClick function to get cursor position")

View file

@ -1,110 +0,0 @@
import re
# Read the ChatInput.tsx file
with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f:
content = f.read()
# Find and replace the handleActionSelect function to handle both / trigger and button click
old_function = ''' const handleActionSelect = (actionId: string) => {
const actionInfo = getActionInfo(actionId);
// Get current cursor position from the RichChatInput
const currentValue = displayValue;
const cursorPosition = actionPopover.cursorPosition || 0;
const beforeCursor = currentValue.slice(0, cursorPosition);
const afterCursor = currentValue.slice(cursorPosition);
const lastSlashIndex = beforeCursor.lastIndexOf('/');
if (lastSlashIndex !== -1) {
const afterSlash = beforeCursor.slice(lastSlashIndex + 1);
// Check if we're still in the same "word" after the slash
if (!afterSlash.includes(' ') && !afterSlash.includes('\n')) {
// Replace the /query with [Action] text
const beforeSlash = currentValue.slice(0, lastSlashIndex);
const actionText = `[${actionInfo.label}]`;
const newValue = beforeSlash + actionText + " " + afterCursor;
setDisplayValue(newValue);
setValue(newValue);
// Set cursor position after the action text and space
const newCursorPosition = lastSlashIndex + actionText.length + 1;
setTimeout(() => {
if (textAreaRef.current) {
textAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition);
textAreaRef.current.focus();
}
}, 0);
}
}
console.log('Action selected:', actionId, 'at position:', cursorPosition);
setActionPopover(prev => ({ ...prev, isOpen: false }));
};'''
new_function = ''' const handleActionSelect = (actionId: string) => {
const actionInfo = getActionInfo(actionId);
// Get current cursor position from the RichChatInput
const currentValue = displayValue;
const cursorPosition = actionPopover.cursorPosition || 0;
const beforeCursor = currentValue.slice(0, cursorPosition);
const afterCursor = currentValue.slice(cursorPosition);
const lastSlashIndex = beforeCursor.lastIndexOf('/');
// Check if this was triggered by a / command (slash exists and no space after it)
if (lastSlashIndex !== -1) {
const afterSlash = beforeCursor.slice(lastSlashIndex + 1);
// Check if we're still in the same "word" after the slash
if (!afterSlash.includes(' ') && !afterSlash.includes('\n')) {
// Replace the /query with [Action] text
const beforeSlash = currentValue.slice(0, lastSlashIndex);
const actionText = `[${actionInfo.label}]`;
const newValue = beforeSlash + actionText + " " + afterCursor;
setDisplayValue(newValue);
setValue(newValue);
// Set cursor position after the action text and space
const newCursorPosition = lastSlashIndex + actionText.length + 1;
setTimeout(() => {
if (textAreaRef.current) {
textAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition);
textAreaRef.current.focus();
}
}, 0);
console.log('Action selected via slash command:', actionId, 'at position:', cursorPosition);
setActionPopover(prev => ({ ...prev, isOpen: false }));
return;
}
}
// If not a slash command, insert action at current cursor position (button click)
const actionText = `[${actionInfo.label}]`;
const newValue = beforeCursor + actionText + " " + afterCursor;
setDisplayValue(newValue);
setValue(newValue);
// Set cursor position after the action text and space
const newCursorPosition = cursorPosition + actionText.length + 1;
setTimeout(() => {
if (textAreaRef.current) {
textAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition);
textAreaRef.current.focus();
}
}, 0);
console.log('Action selected via button click:', actionId, 'at position:', cursorPosition);
setActionPopover(prev => ({ ...prev, isOpen: false }));
};'''
# Replace the function
content = content.replace(old_function, new_function)
# Write back to file
with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f:
f.write(content)
print("Updated handleActionSelect function to handle both slash commands and button clicks")

View file

@ -1 +0,0 @@
temp file

View file

@ -1,156 +0,0 @@
const fs = require('fs');
// Read the file
let content = fs.readFileSync('./ui/desktop/src/components/RichChatInput.tsx', 'utf8');
// Replace the basic spell check function with a smarter one
const oldSpellCheck = `// Simple spell checking function using browser's built-in capabilities
const checkSpelling = async (text: string): Promise<{ word: string; start: number; end: number }[]> => {
// This is a basic implementation - in a real app you might want to use a more sophisticated spell checker
const misspelledWords: { word: string; start: number; end: number }[] = [];
// Test words
const commonMisspellings = [
// Test words
'sdd', 'asdf', 'qwerty', 'test', 'xyz',
// Common misspellings
'teh', 'recieve', 'seperate', 'occured', 'neccessary', 'definately',
'occassion', 'begining', 'tommorrow', 'accomodate', 'existance', 'maintainance',
'alot', 'wierd', 'freind', 'thier', 'calender', 'enviroment', 'goverment',
'independant', 'jewelery', 'liesure', 'mispell', 'noticable', 'occassionally',
'perseverence', 'priviledge', 'recomend', 'rythm', 'sucessful', 'truely',
'untill', 'vaccuum', 'wether', 'wich', 'writting', 'youre', 'its'
];
// Split text into words while preserving positions
const words = text.split(/(\s+|[^\w\s])/);
let currentPos = 0;
for (const word of words) {
const cleanWord = word.toLowerCase().replace(/[^\w]/g, '');
console.log('🔍 SPELL CHECK: Checking word:', word, 'cleaned:', cleanWord);
if (cleanWord && commonMisspellings.includes(cleanWord)) {
console.log('🔍 SPELL CHECK: Found misspelling!', cleanWord);
const start = text.indexOf(word, currentPos);
if (start !== -1) {
misspelledWords.push({
word: word,
start: start,
end: start + word.length
});
console.log('🔍 SPELL CHECK: Added to misspelled array:', { word, start, end: start + word.length });
}
}
currentPos += word.length;
}
return misspelledWords;
};`;
const newSpellCheck = `// Smart spell checking using browser's native capabilities and heuristics
const checkSpelling = async (text: string): Promise<{ word: string; start: number; end: number }[]> => {
const misspelledWords: { word: string; start: number; end: number }[] = [];
// Split text into words while preserving positions
const words = text.split(/(\s+|[^\w\s])/);
let currentPos = 0;
for (const word of words) {
const cleanWord = word.toLowerCase().replace(/[^\w]/g, '');
// Skip very short words, numbers, and common abbreviations
if (cleanWord.length < 3 || /^\d+$/.test(cleanWord)) {
currentPos += word.length;
continue;
}
// Skip common programming terms, file extensions, and technical words
const technicalWords = [
'api', 'url', 'http', 'https', 'json', 'xml', 'css', 'html', 'js', 'ts', 'jsx', 'tsx',
'npm', 'git', 'cli', 'ui', 'ux', 'db', 'sql', 'dev', 'prod', 'env', 'config', 'src',
'app', 'web', 'www', 'com', 'org', 'net', 'io', 'ai', 'ml', 'gpu', 'cpu', 'ram',
'github', 'gitlab', 'docker', 'aws', 'gcp', 'azure', 'k8s', 'oauth', 'jwt', 'cors',
'goose', 'chat', 'llm', 'gpt', 'claude', 'openai', 'anthropic', 'react', 'node',
'typescript', 'javascript', 'python', 'rust', 'java', 'cpp', 'csharp', 'php', 'ruby'
];
if (technicalWords.includes(cleanWord)) {
currentPos += word.length;
continue;
}
// Use heuristic-based spell checking
let isMisspelled = false;
// Check for common patterns of misspellings
if (
// Double letters that shouldn't be doubled
/(.)\1{2,}/.test(cleanWord) ||
// Common letter swaps
/ie/.test(cleanWord) && cleanWord !== 'pie' && cleanWord !== 'tie' && cleanWord !== 'die' ||
// Words ending in 'ey' that should be 'y'
/ey$/.test(cleanWord) && cleanWord.length > 4 ||
// Common misspelling patterns
/seperat/.test(cleanWord) ||
/reciev/.test(cleanWord) ||
/occas/.test(cleanWord) ||
/necess/.test(cleanWord) ||
/definat/.test(cleanWord) ||
/beginn/.test(cleanWord) ||
/accom/.test(cleanWord) ||
/existanc/.test(cleanWord) ||
/maintainanc/.test(cleanWord) ||
/enviroment/.test(cleanWord) ||
/goverment/.test(cleanWord) ||
/independant/.test(cleanWord) ||
/priviledge/.test(cleanWord) ||
/sucessful/.test(cleanWord) ||
/untill/.test(cleanWord) ||
/wether/.test(cleanWord) && cleanWord !== 'whether' ||
// Test words for debugging
cleanWord === 'sdd' || cleanWord === 'asdf' || cleanWord === 'qwerty' ||
cleanWord === 'teh' || cleanWord === 'alot' || cleanWord === 'wierd' ||
cleanWord === 'freind' || cleanWord === 'thier' || cleanWord === 'calender'
) {
isMisspelled = true;
}
// Additional check: words with unusual letter combinations
if (!isMisspelled && cleanWord.length > 4) {
// Check for unusual consonant clusters or vowel patterns
if (
/[bcdfgjklmnpqrstvwxz]{4,}/.test(cleanWord) || // Too many consonants
/[aeiou]{4,}/.test(cleanWord) || // Too many vowels
/q(?!u)/.test(cleanWord) || // Q not followed by U
/[xyz]{2,}/.test(cleanWord) // Multiple x, y, or z
) {
isMisspelled = true;
}
}
if (isMisspelled) {
console.log('🔍 SPELL CHECK: Found misspelling!', cleanWord);
const start = text.indexOf(word, currentPos);
if (start !== -1) {
misspelledWords.push({
word: word,
start: start,
end: start + word.length
});
console.log('🔍 SPELL CHECK: Added to misspelled array:', { word, start, end: start + word.length });
}
}
currentPos += word.length;
}
return misspelledWords;
};`;
content = content.replace(oldSpellCheck, newSpellCheck);
// Write back to file
fs.writeFileSync('./ui/desktop/src/components/RichChatInput.tsx', content);
console.log('Replaced basic spell check with smart heuristic-based spell checker');

View file

@ -1,21 +0,0 @@
// Test if typo-js is working correctly
const Typo = require('typo-js');
console.log('Testing Typo.js...');
console.log('Typo constructor:', typeof Typo);
try {
// Try to create a basic spell checker
const checker = new Typo('en_US');
console.log('Basic checker created:', !!checker);
// Test some words
const testWords = ['hello', 'recieve', 'seperate', 'test'];
testWords.forEach(word => {
const isCorrect = checker.check(word);
console.log(`Word "${word}": ${isCorrect ? 'CORRECT' : 'MISSPELLED'}`);
});
} catch (error) {
console.error('Error creating Typo checker:', error);
}

File diff suppressed because it is too large Load diff

View file

@ -1,504 +0,0 @@
import React, { useRef, useEffect, useState, useCallback, forwardRef, useImperativeHandle } from 'react';
import { ActionPill } from './ActionPill';
import MentionPill from './MentionPill';
import { Zap, Code, FileText, Search, Play, Settings } from 'lucide-react';
interface RichChatInputProps {
value: string;
onChange: (value: string, cursorPos?: number) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
onPaste?: (e: React.ClipboardEvent<HTMLDivElement>) => void;
onFocus?: () => void;
onBlur?: () => void;
onCompositionStart?: () => void;
onCompositionEnd?: () => void;
placeholder?: string;
disabled?: boolean;
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
'data-testid'?: string;
rows?: number;
}
// Action mapping for pill display
const ACTION_MAP = {
'quick-task': { label: 'Quick Task', icon: <Zap size={12} /> },
'generate-code': { label: 'Generate Code', icon: <Code size={12} /> },
'create-document': { label: 'Create Document', icon: <FileText size={12} /> },
'search-files': { label: 'Search Files', icon: <Search size={12} /> },
'run-command': { label: 'Run Command', icon: <Play size={12} /> },
'settings': { label: 'Settings', icon: <Settings size={12} /> },
};
export interface RichChatInputRef {
focus: () => void;
blur: () => void;
setSelectionRange: (start: number, end: number) => void;
getBoundingClientRect: () => DOMRect;
}
export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
value,
onChange,
onKeyDown,
onPaste,
onFocus,
onBlur,
onCompositionStart,
onCompositionEnd,
placeholder,
disabled,
className,
style,
autoFocus,
'data-testid': testId,
rows = 1,
}, ref) => {
const hiddenTextareaRef = useRef<HTMLTextAreaElement>(null);
const displayRef = useRef<HTMLDivElement>(null);
const [isFocused, setIsFocused] = useState(false);
const [cursorPosition, setCursorPosition] = useState(0);
// Expose methods to parent component
useImperativeHandle(ref, () => ({
focus: () => hiddenTextareaRef.current?.focus(),
blur: () => hiddenTextareaRef.current?.blur(),
setSelectionRange: (start: number, end: number) => {
hiddenTextareaRef.current?.setSelectionRange(start, end);
setCursorPosition(start);
},
getBoundingClientRect: () => {
return displayRef.current?.getBoundingClientRect() || new DOMRect();
},
}), []);
// Update cursor position when selection changes
const updateCursorPosition = useCallback(() => {
if (hiddenTextareaRef.current) {
setCursorPosition(hiddenTextareaRef.current.selectionStart);
}
}, []);
// Parse and render content with action pills and cursor
const renderContent = useCallback(() => {
// Show placeholder when there's no text content (regardless of focus state)
if (!value.trim()) {
return (
<div className="relative min-h-[1.5em] flex items-center">
<span className="text-text-muted pointer-events-none select-none">
{placeholder}
</span>
{isFocused && (
<span
className="border-l-2 border-text-default inline-block align-baseline absolute left-0"
style={{
animation: "blink 1s step-end infinite",
height: "1em",
verticalAlign: "baseline",
top: "50%",
transform: "translateY(-50%)"
}}
/>
)}
</div>
);
}
const parts: React.ReactNode[] = [];
const actionRegex = /\[([^\]]+)\]/g;
const mentionRegex = /@([^\s]+)/g; // Match @filename patterns
let lastIndex = 0;
let match;
let keyCounter = 0;
let currentPos = 0; // Track position for cursor placement
// Helper function to add cursor if needed
const addCursorIfNeeded = (position: number) => {
if (isFocused && cursorPosition === position) {
parts.push(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-text-default inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
);
}
};
console.log('🎨 RichChatInput renderContent called with value:', value);
console.log('🔍 Looking for action and mention patterns with regex:', { actionRegex, mentionRegex });
// Find all actions and mentions, then sort by position
const allMatches = [];
// Find all action matches
let actionMatch;
actionRegex.lastIndex = 0; // Reset regex
while ((actionMatch = actionRegex.exec(value)) !== null) {
allMatches.push({
type: 'action',
match: actionMatch,
index: actionMatch.index,
length: actionMatch[0].length,
content: actionMatch[1]
});
}
// Find all mention matches
let mentionMatch;
mentionRegex.lastIndex = 0; // Reset regex
console.log('🔍 Searching for mentions in value:', value);
console.log('🔍 Using mention regex:', mentionRegex);
while ((mentionMatch = mentionRegex.exec(value)) !== null) {
console.log('📁 Found mention match:', mentionMatch);
allMatches.push({
type: 'mention',
match: mentionMatch,
index: mentionMatch.index,
length: mentionMatch[0].length,
content: mentionMatch[1] // filename without @
});
}
// Sort matches by position
allMatches.sort((a, b) => a.index - b.index);
console.log('🔍 All matches found:', allMatches);
console.log('📊 Match breakdown:', {
actions: allMatches.filter(m => m.type === 'action').length,
mentions: allMatches.filter(m => m.type === 'mention').length,
total: allMatches.length
});
// Process all matches in order
for (const matchData of allMatches) {
const { type, match, index, length, content } = matchData;
console.log('✅ Found match:', { type, content, index });
// Add text before this match with potential cursor
const beforeMatch = value.slice(lastIndex, index);
if (beforeMatch) {
let textWithCursor = [];
for (let i = 0; i < beforeMatch.length; i++) {
if (isFocused && cursorPosition === currentPos) {
textWithCursor.push(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-text-default inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
);
}
textWithCursor.push(beforeMatch[i]);
currentPos++;
}
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{textWithCursor}
</span>
);
}
// Add cursor before match if needed
if (isFocused && cursorPosition === currentPos) {
parts.push(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-text-default inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
);
}
if (type === 'action') {
// Handle action pills
const actionLabel = content;
const actionEntry = Object.entries(ACTION_MAP).find(
([_, config]) => config.label === actionLabel
);
console.log('🏷️ Creating action pill:', { actionLabel, actionEntry });
if (actionEntry) {
const [actionId, config] = actionEntry;
parts.push(
<ActionPill
key={`action-${keyCounter++}`}
actionId={actionId}
label={config.label}
icon={config.icon}
variant="default"
size="sm"
onRemove={() => handleRemoveAction(actionLabel)}
/>
);
} else {
// If no matching action, render as text
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{match[0]}
</span>
);
}
} else if (type === 'mention') {
// Handle mention pills
const fileName = content; // filename without @
const filePath = `@${fileName}`; // full mention text
console.log('📁 Creating mention pill:', { fileName, filePath });
parts.push(
<MentionPill
key={`mention-${keyCounter++}`}
fileName={fileName}
filePath={filePath}
variant="default"
size="sm"
onRemove={() => handleRemoveMention(fileName)}
/>
);
}
currentPos += length;
lastIndex = index + length;
}
// Add remaining text with potential cursor
const remainingText = value.slice(lastIndex);
if (remainingText) {
let textWithCursor = [];
for (let i = 0; i < remainingText.length; i++) {
if (isFocused && cursorPosition === currentPos) {
textWithCursor.push(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-text-default inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
);
}
textWithCursor.push(remainingText[i]);
currentPos++;
}
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{textWithCursor}
</span>
);
}
// Add cursor at the end if needed
if (isFocused && cursorPosition === currentPos) {
parts.push(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-text-default inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
);
}
return (
<div className="whitespace-pre-wrap min-h-[1.5em] leading-relaxed">
{parts.length > 0 ? parts : (
isFocused && (
<span className="border-l-2 border-text-default inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
)
)}
</div>
);
}, [value, isFocused, placeholder, cursorPosition]);
const handleRemoveAction = useCallback((actionLabel: string) => {
const actionText = `[${actionLabel}]`;
const newValue = value.replace(actionText, '');
onChange(newValue);
}, [value, onChange]);
const handleRemoveMention = useCallback((fileName: string) => {
const mentionText = `@${fileName}`;
const newValue = value.replace(mentionText, '');
onChange(newValue);
}, [value, onChange]);
const handleTextareaChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
const newCursorPos = e.target.selectionStart;
console.log('🔄 RichChatInput: onChange', { newValue, newCursorPos });
console.log('🎨 Will trigger re-render with new value:', newValue);
onChange(newValue, newCursorPos);
setCursorPosition(newCursorPos);
}, [onChange]);
const handleTextareaKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// Update cursor position on key events
setTimeout(updateCursorPosition, 0);
// Handle backspace on action and mention pills
if (e.key === 'Backspace') {
const cursorPos = e.currentTarget.selectionStart;
const beforeCursor = value.slice(0, cursorPos);
console.log('🔙 Backspace pressed, cursor at:', cursorPos);
console.log('🔙 Text before cursor:', beforeCursor);
// Check if cursor is right after an action pill [Action]
const actionMatch = beforeCursor.match(/\[([^\]]+)\]$/);
if (actionMatch) {
console.log('🔙 Found action pill to remove:', actionMatch[1]);
e.preventDefault();
handleRemoveAction(actionMatch[1]);
return;
}
// Check if cursor is right after a mention pill @filename
const mentionMatch = beforeCursor.match(/@([^\s]+)$/);
if (mentionMatch) {
console.log('🔙 Found mention pill to remove:', mentionMatch[1]);
e.preventDefault();
handleRemoveMention(mentionMatch[1]);
return;
}
}
// Create a proper synthetic event that maintains all the original event properties
const syntheticEvent = {
...e,
key: e.key,
shiftKey: e.shiftKey,
altKey: e.altKey,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
preventDefault: () => e.preventDefault(),
stopPropagation: () => e.stopPropagation(),
currentTarget: {
...e.currentTarget,
value: e.currentTarget.value,
selectionStart: e.currentTarget.selectionStart,
selectionEnd: e.currentTarget.selectionEnd,
getBoundingClientRect: () => displayRef.current?.getBoundingClientRect() || new DOMRect(),
},
target: {
...e.currentTarget,
value: e.currentTarget.value,
selectionStart: e.currentTarget.selectionStart,
selectionEnd: e.currentTarget.selectionEnd,
getBoundingClientRect: () => displayRef.current?.getBoundingClientRect() || new DOMRect(),
},
} as any;
onKeyDown?.(syntheticEvent);
}, [value, handleRemoveAction, onKeyDown, updateCursorPosition]);
const handleTextareaPaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
// Update cursor position after paste
setTimeout(updateCursorPosition, 0);
// Create proper synthetic event
const syntheticEvent = {
...e,
preventDefault: () => e.preventDefault(),
stopPropagation: () => e.stopPropagation(),
clipboardData: e.clipboardData,
currentTarget: displayRef.current,
target: displayRef.current,
} as any;
onPaste?.(syntheticEvent);
}, [onPaste, updateCursorPosition]);
const handleTextareaFocus = useCallback(() => {
setIsFocused(true);
updateCursorPosition();
onFocus?.();
}, [onFocus, updateCursorPosition]);
const handleTextareaBlur = useCallback(() => {
setIsFocused(false);
onBlur?.();
}, [onBlur]);
// Removed handleDisplayClick - let the hidden textarea handle all mouse events naturally
// Handle selection changes (cursor movement)
const handleSelectionChange = useCallback(() => {
if (document.activeElement === hiddenTextareaRef.current) {
updateCursorPosition();
}
}, [updateCursorPosition]);
// Auto-focus effect
useEffect(() => {
if (autoFocus && hiddenTextareaRef.current) {
hiddenTextareaRef.current.focus();
}
}, [autoFocus]);
// Listen for selection changes to update cursor position
useEffect(() => {
document.addEventListener('selectionchange', handleSelectionChange);
return () => {
document.removeEventListener('selectionchange', handleSelectionChange);
};
}, [handleSelectionChange]);
return (
<div className="relative rich-text-input">
{/* Hidden textarea for actual input handling */}
<textarea
ref={hiddenTextareaRef}
value={value}
onChange={handleTextareaChange}
onKeyDown={handleTextareaKeyDown}
onPaste={handleTextareaPaste}
onFocus={handleTextareaFocus}
onBlur={handleTextareaBlur}
onCompositionStart={onCompositionStart}
onCompositionEnd={onCompositionEnd}
disabled={disabled}
data-testid={testId}
className="absolute inset-0 w-full h-full resize-none"
style={{
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
opacity: 0.15, // Slightly more visible for selection
zIndex: 2, // Higher z-index to capture mouse events
background: 'transparent',
border: 'none',
outline: 'none',
resize: 'none',
color: 'rgba(59, 130, 246, 0.2)', // Slightly more visible blue text
caretColor: 'transparent', // Hide caret (we show our own)
pointerEvents: 'auto', // Ensure it can receive mouse events
fontFamily: 'Cash Sans, sans-serif', // Match exact font
fontSize: '0.875rem', // Match text-sm (14px)
lineHeight: '1.5', // Match leading-relaxed
padding: '12px 80px 6px 12px', // Match original: px-3 pt-3 pb-1.5 pr-20
margin: '0',
boxSizing: 'border-box',
whiteSpace: 'pre-wrap', // Match visual display
wordWrap: 'break-word',
}}
className="absolute inset-0 w-full h-full resize-none selection:bg-blue-500 selection:text-white"
rows={rows}
/>
{/* Visual display with action pills and cursor */}
<div
ref={displayRef}
className={`${className} cursor-text relative`}
style={{
...style,
minHeight: `${rows * 1.5}em`,
zIndex: 1, // Lower z-index, behind textarea
pointerEvents: 'none', // Don't interfere with text selection
userSelect: 'none', // Prevent selection on visual layer
WebkitUserSelect: 'none',
fontFamily: 'Cash Sans, sans-serif', // Match textarea font
fontSize: '0.875rem', // Match textarea size
lineHeight: '1.5', // Match textarea line height
padding: '12px 80px 6px 12px', // Match original: px-3 pt-3 pb-1.5 pr-20
margin: '0',
whiteSpace: 'pre-wrap', // Match textarea
wordWrap: 'break-word',
}}
role="textbox"
aria-multiline="true"
aria-placeholder={placeholder}
>
{renderContent()}
</div>
</div>
);
});
RichChatInput.displayName = 'RichChatInput';
export default RichChatInput;