'use client'; import React, { useState, useRef, useEffect } from 'react'; import { cn } from '@/lib/utils'; interface PromptTextareaProps { value: string; onChange: (value: string) => void; placeholder?: string; className?: string; rows?: number; loras?: string[]; embeddings?: string[]; } interface Suggestion { text: string; type: 'lora' | 'embedding'; displayText: string; } export function PromptTextarea({ value, onChange, placeholder, className, rows = 3, loras = [], embeddings = [], }: PromptTextareaProps) { const textareaRef = useRef(null); const [highlighted, setHighlighted] = useState([]); const [suggestions, setSuggestions] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); const [cursorPosition, setCursorPosition] = useState(0); const suggestionsRef = useRef(null); useEffect(() => { highlightSyntax(value); updateSuggestions(value, cursorPosition); }, [value, loras, embeddings, cursorPosition]); const updateSuggestions = (text: string, position: number) => { // Get text before cursor const textBeforeCursor = text.substring(0, position); // Check if we're typing a LoRA const loraMatch = textBeforeCursor.match(/]*)$/); if (loraMatch) { const searchTerm = loraMatch[1].toLowerCase(); const filtered = loras .filter(lora => { const loraBase = lora.replace(/\.(safetensors|ckpt|pt)$/i, ''); return loraBase.toLowerCase().includes(searchTerm); }) .slice(0, 10) .map(lora => ({ text: lora, type: 'lora' as const, displayText: lora.replace(/\.(safetensors|ckpt|pt)$/i, ''), })); if (filtered.length > 0) { setSuggestions(filtered); setShowSuggestions(true); setSelectedIndex(0); return; } } // Check if we're typing an embedding (word boundary) const words = textBeforeCursor.split(/\s+/); const currentWord = words[words.length - 1]; // Only show embedding suggestions if we've typed at least 2 characters // and we're not inside a lora tag if (currentWord.length >= 2 && !textBeforeCursor.match(/]*$/)) { const searchTerm = currentWord.toLowerCase(); const filtered = embeddings .filter(emb => { const embBase = emb.replace(/\.(safetensors|pt)$/i, ''); return embBase.toLowerCase().startsWith(searchTerm); }) .slice(0, 10) .map(emb => ({ text: emb, type: 'embedding' as const, displayText: emb.replace(/\.(safetensors|pt)$/i, ''), })); if (filtered.length > 0) { setSuggestions(filtered); setShowSuggestions(true); setSelectedIndex(0); return; } } setShowSuggestions(false); }; const insertSuggestion = (suggestion: Suggestion) => { if (!textareaRef.current) return; const position = textareaRef.current.selectionStart; const textBefore = value.substring(0, position); const textAfter = value.substring(position); let newText = ''; let newPosition = position; if (suggestion.type === 'lora') { // Find the ]*)$/); if (loraMatch) { const beforeLora = textBefore.substring(0, textBefore.length - loraMatch[0].length); const loraText = ``; newText = beforeLora + loraText + textAfter; newPosition = beforeLora.length + loraText.length; } } else { // Embedding - replace current word const words = textBefore.split(/\s+/); const currentWord = words[words.length - 1]; const beforeWord = textBefore.substring(0, textBefore.length - currentWord.length); newText = beforeWord + suggestion.displayText + textAfter; newPosition = beforeWord.length + suggestion.displayText.length; } onChange(newText); setShowSuggestions(false); // Restore cursor position setTimeout(() => { if (textareaRef.current) { textareaRef.current.selectionStart = newPosition; textareaRef.current.selectionEnd = newPosition; textareaRef.current.focus(); } }, 0); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (!showSuggestions) return; if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex(prev => Math.min(prev + 1, suggestions.length - 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex(prev => Math.max(prev - 1, 0)); } else if (e.key === 'Enter' || e.key === 'Tab') { if (suggestions.length > 0) { e.preventDefault(); insertSuggestion(suggestions[selectedIndex]); } } else if (e.key === 'Escape') { setShowSuggestions(false); } }; const highlightSyntax = (text: string) => { if (!text) { setHighlighted([]); return; } const parts: React.ReactNode[] = []; let lastIndex = 0; const loraNames = new Set( loras.map(name => name.replace(/\.(safetensors|ckpt|pt)$/i, '')) ); const loraFullNames = new Set(loras); const embeddingNames = new Set( embeddings.map(name => name.replace(/\.(safetensors|pt)$/i, '')) ); const loraRegex = /]+):([^>]+)>/g; let match; const matches: Array<{ start: number; end: number; type: 'lora' | 'embedding'; text: string; valid: boolean }> = []; while ((match = loraRegex.exec(text)) !== null) { const loraName = match[1]; const isValid = loraNames.has(loraName) || loraFullNames.has(loraName); matches.push({ start: match.index, end: match.index + match[0].length, type: 'lora', text: match[0], valid: isValid, }); } embeddings.forEach(embedding => { const embeddingBase = embedding.replace(/\.(safetensors|pt)$/i, ''); const embeddingRegex = new RegExp(`\\b${escapeRegex(embeddingBase)}\\b`, 'g'); while ((match = embeddingRegex.exec(text)) !== null) { const isInsideLora = matches.some( m => m.type === 'lora' && match!.index >= m.start && match!.index < m.end ); if (!isInsideLora) { matches.push({ start: match.index, end: match.index + match[0].length, type: 'embedding', text: match[0], valid: true, }); } } }); matches.sort((a, b) => a.start - b.start); matches.forEach((match, index) => { if (match.start > lastIndex) { parts.push( {text.substring(lastIndex, match.start)} ); } const highlightClass = match.type === 'lora' ? match.valid ? 'bg-purple-500/20 text-purple-700 dark:text-purple-300 font-medium rounded px-0.5' : 'bg-red-500/20 text-red-700 dark:text-red-300 font-medium rounded px-0.5' : 'bg-blue-500/20 text-blue-700 dark:text-blue-300 font-medium rounded px-0.5'; parts.push( {match.text} ); lastIndex = match.end; }); if (lastIndex < text.length) { parts.push( {text.substring(lastIndex)} ); } setHighlighted(parts); }; const escapeRegex = (str: string) => { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }; return (