|
|
@@ -35,17 +35,27 @@ export function PromptTextarea({
|
|
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
|
const [cursorPosition, setCursorPosition] = useState(0);
|
|
|
const suggestionsRef = useRef<HTMLDivElement>(null);
|
|
|
+ const justInsertedRef = useRef(false);
|
|
|
|
|
|
useEffect(() => {
|
|
|
highlightSyntax(value);
|
|
|
+
|
|
|
+ // Skip suggestions if we just inserted one
|
|
|
+ if (justInsertedRef.current) {
|
|
|
+ justInsertedRef.current = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
updateSuggestions(value, cursorPosition);
|
|
|
}, [value, loras, embeddings, cursorPosition]);
|
|
|
|
|
|
const updateSuggestions = (text: string, position: number) => {
|
|
|
// Get text before cursor
|
|
|
const textBeforeCursor = text.substring(0, position);
|
|
|
+ const textAfterCursor = text.substring(position);
|
|
|
|
|
|
- // Check if we're typing a LoRA
|
|
|
+ // Check if we're typing a LoRA name (but not in the weight part)
|
|
|
+ // Match: <lora:name| but NOT <lora:name:weight|
|
|
|
const loraMatch = textBeforeCursor.match(/<lora:([^:>]*)$/);
|
|
|
if (loraMatch) {
|
|
|
const searchTerm = loraMatch[1].toLowerCase();
|
|
|
@@ -105,7 +115,7 @@ export function PromptTextarea({
|
|
|
|
|
|
const position = textareaRef.current.selectionStart;
|
|
|
const textBefore = value.substring(0, position);
|
|
|
- const textAfter = value.substring(position);
|
|
|
+ let textAfter = value.substring(position);
|
|
|
|
|
|
let newText = '';
|
|
|
let newPosition = position;
|
|
|
@@ -115,6 +125,15 @@ export function PromptTextarea({
|
|
|
const loraMatch = textBefore.match(/<lora:([^:>]*)$/);
|
|
|
if (loraMatch) {
|
|
|
const beforeLora = textBefore.substring(0, textBefore.length - loraMatch[0].length);
|
|
|
+
|
|
|
+ // Check if we're editing an existing tag
|
|
|
+ // Remove everything until the closing > (rest of name, weight, closing bracket)
|
|
|
+ const afterLoraMatch = textAfter.match(/^[^<>]*>/);
|
|
|
+ if (afterLoraMatch) {
|
|
|
+ // Remove the old tag remainder
|
|
|
+ textAfter = textAfter.substring(afterLoraMatch[0].length);
|
|
|
+ }
|
|
|
+
|
|
|
const loraText = `<lora:${suggestion.displayText}:0.8>`;
|
|
|
newText = beforeLora + loraText + textAfter;
|
|
|
newPosition = beforeLora.length + loraText.length;
|
|
|
@@ -128,6 +147,9 @@ export function PromptTextarea({
|
|
|
newPosition = beforeWord.length + suggestion.displayText.length;
|
|
|
}
|
|
|
|
|
|
+ // Mark that we just inserted a suggestion to prevent retriggering
|
|
|
+ justInsertedRef.current = true;
|
|
|
+
|
|
|
onChange(newText);
|
|
|
setShowSuggestions(false);
|
|
|
|
|
|
@@ -254,7 +276,17 @@ export function PromptTextarea({
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
- <div className="relative w-full pointer-events-none">
|
|
|
+ <div className="relative w-full">
|
|
|
+ <div
|
|
|
+ className="absolute inset-0 pointer-events-none px-3 py-2 text-sm font-mono whitespace-pre-wrap break-words overflow-hidden rounded-md text-foreground"
|
|
|
+ style={{
|
|
|
+ zIndex: 1,
|
|
|
+ WebkitFontSmoothing: 'antialiased',
|
|
|
+ MozOsxFontSmoothing: 'grayscale'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {highlighted.length > 0 ? highlighted : value}
|
|
|
+ </div>
|
|
|
<textarea
|
|
|
ref={textareaRef}
|
|
|
value={value}
|
|
|
@@ -267,17 +299,20 @@ export function PromptTextarea({
|
|
|
placeholder={placeholder}
|
|
|
rows={rows}
|
|
|
className={cn(
|
|
|
- 'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 font-mono resize-none relative pointer-events-auto',
|
|
|
+ 'prompt-textarea-input flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 font-mono resize-none relative',
|
|
|
className
|
|
|
)}
|
|
|
- style={{ background: 'transparent' }}
|
|
|
+ style={{
|
|
|
+ background: 'transparent',
|
|
|
+ color: 'transparent',
|
|
|
+ WebkitTextFillColor: 'transparent',
|
|
|
+ caretColor: 'hsl(var(--foreground))',
|
|
|
+ zIndex: 2,
|
|
|
+ textShadow: 'none'
|
|
|
+ } as React.CSSProperties & {
|
|
|
+ caretColor: string;
|
|
|
+ }}
|
|
|
/>
|
|
|
- <div
|
|
|
- className="absolute inset-0 pointer-events-none px-3 py-2 text-sm font-mono whitespace-pre-wrap break-words overflow-hidden rounded-md -z-10"
|
|
|
- style={{ color: 'transparent' }}
|
|
|
- >
|
|
|
- {highlighted.length > 0 ? highlighted : value}
|
|
|
- </div>
|
|
|
|
|
|
{/* Autocomplete Suggestions */}
|
|
|
{showSuggestions && suggestions.length > 0 && (
|