mirror of
https://github.com/block/goose.git
synced 2026-04-28 03:29:36 +00:00
Clean up: remove backup files, temp scripts, and debug files
Some checks are pending
Documentation Site Preview / deploy (push) Waiting to run
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:
parent
80cf2f10ee
commit
30607faed6
8 changed files with 0 additions and 2694 deletions
|
|
@ -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" }
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -1 +0,0 @@
|
|||
temp 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');
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue