From 54e54d6d7e7eadc1d3b67054039e6bc70e643b50 Mon Sep 17 00:00:00 2001 From: colinislit Date: Sat, 3 Jan 2026 12:27:48 +0100 Subject: [PATCH] fix(cortex): add @mention detection to ChatInput (not just CommandInput) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The @mention dropdown was only in command-input.tsx but users type in chat-input.tsx (the v3.0 chat textarea). Added @mention support there: - @mention state and detection logic - PatientMentionDropdown rendering - handleMentionSelect with activePatient setting - Escape key closes dropdown before clearing input 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- components/cortex/chat/chat-input.tsx | 67 ++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/components/cortex/chat/chat-input.tsx b/components/cortex/chat/chat-input.tsx index eb848e2..9c28e22 100644 --- a/components/cortex/chat/chat-input.tsx +++ b/components/cortex/chat/chat-input.tsx @@ -13,6 +13,9 @@ import { useState, useRef, KeyboardEvent, ChangeEvent, forwardRef, useImperative import { Send, Mic } from 'lucide-react'; import { useCortexStore } from '@/stores/cortex-store'; import { cn } from '@/lib/utils'; +import { usePatientSelection } from '@/lib/cortex/hooks/use-patient-selection'; +import type { PatientSearchResult } from '@/lib/cortex/hooks/use-patient-search'; +import { PatientMentionDropdown } from '../command-center/patient-mention-dropdown'; interface ChatInputProps { placeholder?: string; @@ -35,6 +38,17 @@ export const ChatInput = forwardRef(function Ch const textareaRef = useRef(null); const addChatMessage = useCortexStore((s) => s.addChatMessage); + // @mention state (E2) + const [mentionState, setMentionState] = useState<{ + query: string; + startIndex: number; + } | null>(null); + + // Patient selection hook + const { selectPatient } = usePatientSelection({ + showSuccessToast: false, + }); + // Expose focus, clear, and setValue methods to parent useImperativeHandle(ref, () => ({ focus: () => { @@ -63,9 +77,45 @@ export const ChatInput = forwardRef(function Ch }, })); + // @mention detection + const detectMention = (value: string) => { + const lastAtIndex = value.lastIndexOf('@'); + if (lastAtIndex !== -1) { + const afterAt = value.slice(lastAtIndex + 1); + // Active if: no space after @, and at least 1 char + if (!afterAt.includes(' ') && afterAt.length > 0) { + setMentionState({ query: afterAt, startIndex: lastAtIndex }); + return; + } + } + setMentionState(null); + }; + + // Handle @mention selection + const handleMentionSelect = (patient: PatientSearchResult) => { + if (!mentionState) return; + + // Replace @query with @name in input + const before = inputValue.slice(0, mentionState.startIndex); + const after = inputValue.slice(mentionState.startIndex + mentionState.query.length + 1); + const newValue = `${before}@${patient.name} ${after}`.trim(); + setInputValue(newValue); + + // Set activePatient + selectPatient(patient); + + // Close dropdown + setMentionState(null); + + // Focus back on textarea + textareaRef.current?.focus(); + }; + // Handle input change and auto-resize const handleChange = (e: ChangeEvent) => { - setInputValue(e.target.value); + const value = e.target.value; + setInputValue(value); + detectMention(value); // Auto-resize textarea if (textareaRef.current) { @@ -108,9 +158,13 @@ export const ChatInput = forwardRef(function Ch handleSubmit(); } - // Escape to clear input + // Escape: close dropdown first, then clear input if (e.key === 'Escape') { e.preventDefault(); + if (mentionState) { + setMentionState(null); + return; + } setInputValue(''); if (textareaRef.current) { textareaRef.current.style.height = 'auto'; @@ -123,6 +177,15 @@ export const ChatInput = forwardRef(function Ch return (
+ {/* @mention dropdown */} + {mentionState && ( + setMentionState(null)} + /> + )} + {/* Textarea input */}