Se funcionalidad para abrir ventana de sugerencias dentro del texto, se agrega autocompletado
fix #141
This commit is contained in:
@@ -126,7 +126,7 @@ function RouteComponent() {
|
|||||||
const [optimisticMessage, setOptimisticMessage] = useState<string | null>(
|
const [optimisticMessage, setOptimisticMessage] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
|
const [filterQuery, setFilterQuery] = useState('')
|
||||||
const availableFields = useMemo(() => {
|
const availableFields = useMemo(() => {
|
||||||
const definicion = data?.estructuras_plan
|
const definicion = data?.estructuras_plan
|
||||||
?.definicion as EstructuraDefinicion
|
?.definicion as EstructuraDefinicion
|
||||||
@@ -140,6 +140,15 @@ function RouteComponent() {
|
|||||||
value: String(value.description || ''),
|
value: String(value.description || ''),
|
||||||
}))
|
}))
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
|
const filteredFields = useMemo(() => {
|
||||||
|
return availableFields.filter(
|
||||||
|
(field) =>
|
||||||
|
field.label.toLowerCase().includes(filterQuery.toLowerCase()) &&
|
||||||
|
!selectedFields.some((s) => s.key === field.key), // No mostrar ya seleccionados
|
||||||
|
)
|
||||||
|
}, [availableFields, filterQuery, selectedFields])
|
||||||
|
|
||||||
const activeChatData = useMemo(() => {
|
const activeChatData = useMemo(() => {
|
||||||
return lastConversation?.find((chat: any) => chat.id === activeChatId)
|
return lastConversation?.find((chat: any) => chat.id === activeChatId)
|
||||||
}, [lastConversation, activeChatId])
|
}, [lastConversation, activeChatId])
|
||||||
@@ -220,6 +229,18 @@ function RouteComponent() {
|
|||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, [chatMessages, isLoading])
|
}, [chatMessages, isLoading])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Verificamos cuáles campos de la lista "selectedFields" ya no están presentes en el texto del input
|
||||||
|
const camposActualizados = selectedFields.filter((field) =>
|
||||||
|
input.includes(field.label),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Solo actualizamos el estado si hubo un cambio real (para evitar bucles infinitos)
|
||||||
|
if (camposActualizados.length !== selectedFields.length) {
|
||||||
|
setSelectedFields(camposActualizados)
|
||||||
|
}
|
||||||
|
}, [input, selectedFields])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoadingConv || !lastConversation) return
|
if (isLoadingConv || !lastConversation) return
|
||||||
|
|
||||||
@@ -311,8 +332,20 @@ function RouteComponent() {
|
|||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const val = e.target.value
|
const val = e.target.value
|
||||||
|
const cursorPosition = e.target.selectionStart // Dónde está escribiendo el usuario
|
||||||
setInput(val)
|
setInput(val)
|
||||||
setShowSuggestions(val.endsWith(':'))
|
|
||||||
|
// Busca un ":" seguido de letras justo antes del cursor
|
||||||
|
const textBeforeCursor = val.slice(0, cursorPosition)
|
||||||
|
const match = textBeforeCursor.match(/:(\w*)$/)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
setShowSuggestions(true)
|
||||||
|
setFilterQuery(match[1]) // Esto es lo que se usa para el filtrado
|
||||||
|
} else {
|
||||||
|
setShowSuggestions(false)
|
||||||
|
setFilterQuery('')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const injectFieldsIntoInput = (
|
const injectFieldsIntoInput = (
|
||||||
@@ -329,28 +362,22 @@ function RouteComponent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleField = (field: SelectedField) => {
|
const toggleField = (field: SelectedField) => {
|
||||||
let isAdding = false
|
// 1. Lo agregamos a la lista de "SelectedFields" (para que la IA sepa qué procesar)
|
||||||
|
|
||||||
setSelectedFields((prev) => {
|
setSelectedFields((prev) => {
|
||||||
const isSelected = prev.find((f) => f.key === field.key)
|
const isSelected = prev.find((f) => f.key === field.key)
|
||||||
if (isSelected) {
|
return isSelected ? prev : [...prev, field]
|
||||||
return prev.filter((f) => f.key !== field.key)
|
|
||||||
} else {
|
|
||||||
isAdding = true
|
|
||||||
return [...prev, field]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 2. Insertamos el nombre del campo en el texto exactamente donde estaba el ":"
|
||||||
setInput((prev) => {
|
setInput((prev) => {
|
||||||
const cleanPrev = prev.replace(/:/g, '').trim()
|
// Reemplaza el último ":" y cualquier texto de filtro por el label del campo
|
||||||
|
const nuevoTexto = prev.replace(/:(\w*)$/, field.label)
|
||||||
if (cleanPrev === '') {
|
return nuevoTexto + ' ' // Añadimos un espacio para que el usuario siga escribiendo
|
||||||
return `${field.label} `
|
|
||||||
}
|
|
||||||
return `${cleanPrev} ${field.label} `
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 3. Limpiamos estados de búsqueda
|
||||||
setShowSuggestions(false)
|
setShowSuggestions(false)
|
||||||
|
setFilterQuery('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildPrompt = (userInput: string, fields: Array<SelectedField>) => {
|
const buildPrompt = (userInput: string, fields: Array<SelectedField>) => {
|
||||||
@@ -707,42 +734,35 @@ function RouteComponent() {
|
|||||||
<div className="relative mx-auto max-w-4xl">
|
<div className="relative mx-auto max-w-4xl">
|
||||||
{/* MENÚ DE SUGERENCIAS FLOTANTE */}
|
{/* MENÚ DE SUGERENCIAS FLOTANTE */}
|
||||||
{showSuggestions && (
|
{showSuggestions && (
|
||||||
<div className="...">
|
<div className="animate-in slide-in-from-bottom-2 absolute bottom-full mb-2 w-full rounded-xl border bg-white shadow-2xl">
|
||||||
<div className="...">Seleccionar campo para IA</div>
|
<div className="border-b bg-slate-50 px-3 py-2 text-[10px] font-bold text-slate-500 uppercase">
|
||||||
|
Resultados para "{filterQuery}"
|
||||||
|
</div>
|
||||||
<div className="max-h-64 overflow-y-auto p-1">
|
<div className="max-h-64 overflow-y-auto p-1">
|
||||||
{availableFields.map((field) => {
|
{filteredFields.length > 0 ? (
|
||||||
// 1. Verificamos si el campo ya está en la lista de seleccionados
|
filteredFields.map((field, index) => (
|
||||||
const isAlreadySelected = selectedFields.some(
|
|
||||||
(f) => f.key === field.key,
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
<button
|
||||||
key={field.key}
|
key={field.key}
|
||||||
onClick={() => !isAlreadySelected && toggleField(field)}
|
onClick={() => toggleField(field)}
|
||||||
// 2. Aplicamos el atributo disabled
|
className={`flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm transition-colors ${
|
||||||
disabled={isAlreadySelected}
|
index === 0
|
||||||
className={`group flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm transition-colors ${
|
? 'bg-teal-50 text-teal-700 ring-1 ring-teal-200 ring-inset'
|
||||||
isAlreadySelected
|
: 'hover:bg-slate-50'
|
||||||
? 'cursor-not-allowed bg-slate-50 opacity-50' // Estilo visual de deshabilitado
|
|
||||||
: 'hover:bg-teal-50'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<span>{field.label}</span>
|
||||||
className={`text-slate-700 ${!isAlreadySelected && 'group-hover:text-teal-700'}`}
|
{index === 0 && (
|
||||||
>
|
<span className="font-mono text-[10px] opacity-50">
|
||||||
{field.label}
|
TAB
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{isAlreadySelected && (
|
|
||||||
<div className="flex items-center gap-1 text-[10px] font-medium text-teal-600">
|
|
||||||
<Check size={12} />
|
|
||||||
<span>Seleccionado</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)
|
))
|
||||||
})}
|
) : (
|
||||||
|
<div className="p-3 text-center text-xs text-slate-400">
|
||||||
|
No hay coincidencias
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -775,8 +795,30 @@ function RouteComponent() {
|
|||||||
value={input}
|
value={input}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
// Enter envía, Shift+Enter hace salto de línea
|
if (showSuggestions) {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
||||||
|
if (filteredFields.length > 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
toggleField(filteredFields[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
setShowSuggestions(false)
|
||||||
|
setFilterQuery('')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Si el usuario borra y el input está vacío, eliminar el último campo
|
||||||
|
if (
|
||||||
|
e.key === 'Backspace' &&
|
||||||
|
input === '' &&
|
||||||
|
selectedFields.length > 0
|
||||||
|
) {
|
||||||
|
setSelectedFields((prev) => prev.slice(0, -1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey && !showSuggestions) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!isSending) handleSend()
|
if (!isSending) handleSend()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user