Merge pull request 'Ajuste de vista para las tablas #152' (#153) from issue/152-ajuste-de-vista-para-las-tablas into main
Reviewed-on: #153
This commit was merged in pull request #153.
This commit is contained in:
@@ -8,6 +8,7 @@ export const ImprovementCard = ({
|
|||||||
suggestions,
|
suggestions,
|
||||||
onApply,
|
onApply,
|
||||||
planId,
|
planId,
|
||||||
|
dbMessageId,
|
||||||
currentDatos,
|
currentDatos,
|
||||||
activeChatId,
|
activeChatId,
|
||||||
onApplySuccess,
|
onApplySuccess,
|
||||||
@@ -16,6 +17,7 @@ export const ImprovementCard = ({
|
|||||||
onApply?: (key: string, value: string) => void
|
onApply?: (key: string, value: string) => void
|
||||||
planId: string
|
planId: string
|
||||||
currentDatos: any
|
currentDatos: any
|
||||||
|
dbMessageId: string
|
||||||
activeChatId: any
|
activeChatId: any
|
||||||
onApplySuccess?: (key: string) => void
|
onApplySuccess?: (key: string) => void
|
||||||
}) => {
|
}) => {
|
||||||
@@ -53,9 +55,11 @@ export const ImprovementCard = ({
|
|||||||
setLocalApplied((prev) => [...prev, key])
|
setLocalApplied((prev) => [...prev, key])
|
||||||
|
|
||||||
if (onApplySuccess) onApplySuccess(key)
|
if (onApplySuccess) onApplySuccess(key)
|
||||||
if (activeChatId) {
|
|
||||||
|
// --- CAMBIO AQUÍ: Ahora enviamos el ID del mensaje ---
|
||||||
|
if (dbMessageId) {
|
||||||
updateAppliedStatus.mutate({
|
updateAppliedStatus.mutate({
|
||||||
conversacionId: activeChatId,
|
conversacionId: dbMessageId, // Cambiamos el nombre de la propiedad si es necesario
|
||||||
campoAfectado: key,
|
campoAfectado: key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export async function library_search(payload: {
|
|||||||
export async function create_conversation(planId: string) {
|
export async function create_conversation(planId: string) {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
const { data, error } = await supabase.functions.invoke(
|
const { data, error } = await supabase.functions.invoke(
|
||||||
'create-chat-conversation/conversations',
|
'create-chat-conversation/plan/conversations',
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
@@ -149,7 +149,7 @@ export async function ai_plan_chat_v2(payload: {
|
|||||||
}): Promise<{ reply: string; meta?: any }> {
|
}): Promise<{ reply: string; meta?: any }> {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
const { data, error } = await supabase.functions.invoke(
|
const { data, error } = await supabase.functions.invoke(
|
||||||
`create-chat-conversation/conversations/${payload.conversacionId}/messages`,
|
`create-chat-conversation/conversations/plan/${payload.conversacionId}/messages`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
@@ -175,6 +175,22 @@ export async function getConversationByPlan(planId: string) {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
return data ?? []
|
return data ?? []
|
||||||
}
|
}
|
||||||
|
export async function getMessagesByConversation(conversationId: string) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('plan_mensajes_ia')
|
||||||
|
.select('*')
|
||||||
|
.eq('conversacion_plan_id', conversationId)
|
||||||
|
.order('fecha_creacion', { ascending: true }) // Ascendente para que el chat fluya en orden cronológico
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error al obtener mensajes:', error.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return data ?? []
|
||||||
|
}
|
||||||
|
|
||||||
export async function update_conversation_title(
|
export async function update_conversation_title(
|
||||||
conversacionId: string,
|
conversacionId: string,
|
||||||
@@ -194,45 +210,40 @@ export async function update_conversation_title(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function update_recommendation_applied_status(
|
export async function update_recommendation_applied_status(
|
||||||
conversacionId: string,
|
mensajeId: string, // Ahora es más eficiente usar el ID del mensaje directamente
|
||||||
campoAfectado: string,
|
campoAfectado: string,
|
||||||
) {
|
) {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
// 1. Obtener el estado actual del JSON
|
// 1. Obtener la propuesta actual de ese mensaje específico
|
||||||
const { data: conv, error: fetchError } = await supabase
|
const { data: msgData, error: fetchError } = await supabase
|
||||||
.from('conversaciones_plan')
|
.from('plan_mensajes_ia')
|
||||||
.select('conversacion_json')
|
.select('propuesta')
|
||||||
.eq('id', conversacionId)
|
.eq('id', mensajeId)
|
||||||
.single()
|
.single()
|
||||||
|
|
||||||
if (fetchError) throw fetchError
|
if (fetchError) throw fetchError
|
||||||
if (!conv.conversacion_json) throw new Error('No se encontró la conversación')
|
if (!msgData?.propuesta)
|
||||||
|
throw new Error('No se encontró la propuesta en el mensaje')
|
||||||
|
|
||||||
// 2. Transformar el JSON para marcar como aplicada la recomendación específica
|
const propuestaActual = msgData.propuesta as any
|
||||||
// Usamos una transformación inmutable para evitar efectos secundarios
|
|
||||||
const nuevoJson = (conv.conversacion_json as Array<any>).map((msg) => {
|
// 2. Modificar el array de recommendations dentro de la propuesta
|
||||||
if (msg.user === 'assistant' && Array.isArray(msg.recommendations)) {
|
// Mantenemos el resto de la propuesta (prompt, respuesta, etc.) intacto
|
||||||
return {
|
const nuevaPropuesta = {
|
||||||
...msg,
|
...propuestaActual,
|
||||||
recommendations: msg.recommendations.map((rec: any) =>
|
recommendations: (propuestaActual.recommendations || []).map((rec: any) =>
|
||||||
rec.campo_afectado === campoAfectado
|
rec.campo_afectado === campoAfectado ? { ...rec, aplicada: true } : rec,
|
||||||
? { ...rec, aplicada: true }
|
|
||||||
: rec,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return msg
|
|
||||||
})
|
|
||||||
|
|
||||||
// 3. Actualizar la base de datos con el nuevo JSON
|
// 3. Actualizar la base de datos con el nuevo objeto JSON
|
||||||
const { data, error: updateError } = await supabase
|
const { error: updateError } = await supabase
|
||||||
.from('conversaciones_plan')
|
.from('plan_mensajes_ia')
|
||||||
.update({ conversacion_json: nuevoJson })
|
.update({ propuesta: nuevaPropuesta })
|
||||||
.eq('id', conversacionId)
|
.eq('id', mensajeId)
|
||||||
.select()
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (updateError) throw updateError
|
if (updateError) throw updateError
|
||||||
return data
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
update_conversation_status,
|
update_conversation_status,
|
||||||
update_recommendation_applied_status,
|
update_recommendation_applied_status,
|
||||||
update_conversation_title,
|
update_conversation_title,
|
||||||
|
getMessagesByConversation,
|
||||||
} from '../api/ai.api'
|
} from '../api/ai.api'
|
||||||
|
|
||||||
// eslint-disable-next-line node/prefer-node-protocol
|
// eslint-disable-next-line node/prefer-node-protocol
|
||||||
@@ -88,6 +89,25 @@ export function useConversationByPlan(planId: string | null) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useMessagesByChat(conversationId: string | null) {
|
||||||
|
return useQuery({
|
||||||
|
// La queryKey debe ser única; incluimos el ID para que se refresque al cambiar de chat
|
||||||
|
queryKey: ['conversation-messages', conversationId],
|
||||||
|
|
||||||
|
// Solo ejecutamos la función si el ID no es null o undefined
|
||||||
|
queryFn: () => {
|
||||||
|
if (!conversationId) throw new Error('Conversation ID is required')
|
||||||
|
return getMessagesByConversation(conversationId)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Importante: 'enabled' controla que no se dispare la petición si no hay ID
|
||||||
|
enabled: !!conversationId,
|
||||||
|
|
||||||
|
// Opcional: Mantener los datos previos mientras se carga la nueva conversación
|
||||||
|
placeholderData: (previousData) => previousData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function useUpdateRecommendationApplied() {
|
export function useUpdateRecommendationApplied() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { Textarea } from '@/components/ui/textarea'
|
|||||||
import {
|
import {
|
||||||
useAIPlanChat,
|
useAIPlanChat,
|
||||||
useConversationByPlan,
|
useConversationByPlan,
|
||||||
|
useMessagesByChat,
|
||||||
useUpdateConversationStatus,
|
useUpdateConversationStatus,
|
||||||
useUpdateConversationTitle,
|
useUpdateConversationTitle,
|
||||||
} from '@/data'
|
} from '@/data'
|
||||||
@@ -103,6 +104,8 @@ function RouteComponent() {
|
|||||||
)
|
)
|
||||||
const { data: lastConversation, isLoading: isLoadingConv } =
|
const { data: lastConversation, isLoading: isLoadingConv } =
|
||||||
useConversationByPlan(planId)
|
useConversationByPlan(planId)
|
||||||
|
const { data: mensajesDelChat, isLoading: isLoadingMessages } =
|
||||||
|
useMessagesByChat(activeChatId)
|
||||||
const [selectedArchivoIds, setSelectedArchivoIds] = useState<Array<string>>(
|
const [selectedArchivoIds, setSelectedArchivoIds] = useState<Array<string>>(
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
@@ -154,36 +157,32 @@ function RouteComponent() {
|
|||||||
}, [lastConversation, activeChatId])
|
}, [lastConversation, activeChatId])
|
||||||
|
|
||||||
const chatMessages = useMemo(() => {
|
const chatMessages = useMemo(() => {
|
||||||
// 1. Si no hay ID o no hay data del chat, retornamos vacío
|
if (!activeChatId || !mensajesDelChat) return []
|
||||||
if (!activeChatId || !activeChatData) return []
|
|
||||||
|
|
||||||
const json = (activeChatData.conversacion_json ||
|
// flatMap nos permite devolver 2 elementos (pregunta y respuesta) por cada registro de la BD
|
||||||
[]) as unknown as Array<ChatMessageJSON>
|
return mensajesDelChat.flatMap((msg: any) => {
|
||||||
|
const messages = []
|
||||||
|
|
||||||
// 2. Verificamos que 'json' sea realmente un array antes de mapear
|
// 1. Mensaje del Usuario
|
||||||
if (!Array.isArray(json)) return []
|
messages.push({
|
||||||
|
id: `${msg.id}-user`,
|
||||||
|
role: 'user',
|
||||||
|
content: msg.mensaje,
|
||||||
|
selectedFields: msg.campos || [], // Aquí están tus campos
|
||||||
|
})
|
||||||
|
|
||||||
return json.map((msg, index: number) => {
|
// 2. Mensaje del Asistente (si hay respuesta)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
if (msg.respuesta) {
|
||||||
if (!msg?.user) {
|
// Extraemos las recomendaciones de la nueva estructura: msg.propuesta.recommendations
|
||||||
return {
|
const rawRecommendations = msg.propuesta?.recommendations || []
|
||||||
id: `err-${index}`,
|
|
||||||
|
messages.push({
|
||||||
|
id: `${msg.id}-ai`,
|
||||||
|
dbMessageId: msg.id,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: '',
|
content: msg.respuesta,
|
||||||
suggestions: [],
|
isRefusal: msg.is_refusal,
|
||||||
}
|
suggestions: rawRecommendations.map((rec: any) => {
|
||||||
}
|
|
||||||
|
|
||||||
const isAssistant = msg.user === 'assistant'
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `${activeChatId}-${index}`,
|
|
||||||
role: isAssistant ? 'assistant' : 'user',
|
|
||||||
content: isAssistant ? msg.message || '' : msg.prompt || '', // Agregamos fallback a string vacío
|
|
||||||
isRefusal: isAssistant && msg.refusal === true,
|
|
||||||
suggestions:
|
|
||||||
isAssistant && msg.recommendations
|
|
||||||
? msg.recommendations.map((rec) => {
|
|
||||||
const fieldConfig = availableFields.find(
|
const fieldConfig = availableFields.find(
|
||||||
(f) => f.key === rec.campo_afectado,
|
(f) => f.key === rec.campo_afectado,
|
||||||
)
|
)
|
||||||
@@ -195,12 +194,13 @@ function RouteComponent() {
|
|||||||
newValue: rec.texto_mejora,
|
newValue: rec.texto_mejora,
|
||||||
applied: rec.aplicada,
|
applied: rec.aplicada,
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
: [],
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}, [activeChatData, activeChatId, availableFields])
|
|
||||||
|
|
||||||
|
return messages
|
||||||
|
})
|
||||||
|
}, [mensajesDelChat, activeChatId, availableFields])
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
// Buscamos el viewport interno del ScrollArea de Radix
|
// Buscamos el viewport interno del ScrollArea de Radix
|
||||||
@@ -226,6 +226,8 @@ function RouteComponent() {
|
|||||||
}, [lastConversation])
|
}, [lastConversation])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log(mensajesDelChat)
|
||||||
|
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, [chatMessages, isLoading])
|
}, [chatMessages, isLoading])
|
||||||
|
|
||||||
@@ -242,30 +244,38 @@ function RouteComponent() {
|
|||||||
}, [input, selectedFields])
|
}, [input, selectedFields])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoadingConv || !lastConversation) return
|
if (isLoadingConv || isSending) return
|
||||||
|
|
||||||
const isChatStillActive = activeChats.some(
|
const currentChatExists = activeChats.some(
|
||||||
(chat) => chat.id === activeChatId,
|
(chat) => chat.id === activeChatId,
|
||||||
)
|
)
|
||||||
const isCreationMode = messages.length === 1 && messages[0].id === 'welcome'
|
const isCreationMode = messages.length === 1 && messages[0].id === 'welcome'
|
||||||
|
|
||||||
// Caso A: El chat actual ya no es válido (fue archivado o borrado)
|
// 1. Si el chat que teníamos seleccionado ya no existe (ej. se archivó)
|
||||||
if (activeChatId && !isChatStillActive && !isCreationMode) {
|
if (activeChatId && !currentChatExists && !isCreationMode) {
|
||||||
setActiveChatId(undefined)
|
setActiveChatId(undefined)
|
||||||
setMessages([])
|
setMessages([])
|
||||||
return // Salimos para evitar ejecuciones extra en este render
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caso B: No hay chat seleccionado y hay chats disponibles (Auto-selección al cargar)
|
// 2. Auto-selección inicial: Solo si no hay ID, no estamos creando y hay chats
|
||||||
if (!activeChatId && activeChats.length > 0 && !isCreationMode) {
|
if (
|
||||||
|
!activeChatId &&
|
||||||
|
activeChats.length > 0 &&
|
||||||
|
!isCreationMode &&
|
||||||
|
chatMessages.length === 0
|
||||||
|
) {
|
||||||
setActiveChatId(activeChats[0].id)
|
setActiveChatId(activeChats[0].id)
|
||||||
}
|
}
|
||||||
|
}, [
|
||||||
|
activeChats,
|
||||||
|
activeChatId,
|
||||||
|
isLoadingConv,
|
||||||
|
isSending,
|
||||||
|
messages.length,
|
||||||
|
chatMessages.length,
|
||||||
|
])
|
||||||
|
|
||||||
// Caso C: Si la lista de chats está vacía y no estamos creando uno, limpiar por si acaso
|
|
||||||
if (activeChats.length === 0 && activeChatId && !isCreationMode) {
|
|
||||||
setActiveChatId(undefined)
|
|
||||||
}
|
|
||||||
}, [activeChats, activeChatId, isLoadingConv, messages.length])
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const state = routerState.location.state as any
|
const state = routerState.location.state as any
|
||||||
if (!state?.campo_edit || availableFields.length === 0) return
|
if (!state?.campo_edit || availableFields.length === 0) return
|
||||||
@@ -352,13 +362,16 @@ function RouteComponent() {
|
|||||||
input: string,
|
input: string,
|
||||||
fields: Array<SelectedField>,
|
fields: Array<SelectedField>,
|
||||||
) => {
|
) => {
|
||||||
const cleaned = input.replace(/\n?\[Campos:[^\]]*]/g, '').trim()
|
// 1. Limpiamos cualquier rastro anterior de la etiqueta (por si acaso)
|
||||||
|
// Esta regex ahora también limpia si el texto termina de forma natural
|
||||||
|
const cleaned = input.replace(/[:\s]+[^:]*$/, '').trim()
|
||||||
|
|
||||||
if (fields.length === 0) return cleaned
|
if (fields.length === 0) return cleaned
|
||||||
|
|
||||||
const fieldLabels = fields.map((f) => f.label).join(', ')
|
const fieldLabels = fields.map((f) => f.label).join(', ')
|
||||||
|
|
||||||
return `${cleaned}\n[Campos: ${fieldLabels}]`
|
// 2. Devolvemos un formato natural: "Mejora este campo: Nombre del Campo"
|
||||||
|
return `${cleaned}: ${fieldLabels}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleField = (field: SelectedField) => {
|
const toggleField = (field: SelectedField) => {
|
||||||
@@ -388,42 +401,46 @@ function RouteComponent() {
|
|||||||
|
|
||||||
const handleSend = async (promptOverride?: string) => {
|
const handleSend = async (promptOverride?: string) => {
|
||||||
const rawText = promptOverride || input
|
const rawText = promptOverride || input
|
||||||
if (!rawText.trim() && selectedFields.length === 0) return
|
|
||||||
if (isSending || (!rawText.trim() && selectedFields.length === 0)) return
|
if (isSending || (!rawText.trim() && selectedFields.length === 0)) return
|
||||||
|
|
||||||
const currentFields = [...selectedFields]
|
const currentFields = [...selectedFields]
|
||||||
const finalPrompt = buildPrompt(rawText, currentFields)
|
|
||||||
setIsSending(true)
|
setIsSending(true)
|
||||||
setOptimisticMessage(rawText)
|
setOptimisticMessage(rawText)
|
||||||
setInput('')
|
|
||||||
setSelectedArchivoIds([])
|
|
||||||
setSelectedRepositorioIds([])
|
|
||||||
setUploadedFiles([])
|
|
||||||
try {
|
|
||||||
const payload: any = {
|
|
||||||
planId: planId,
|
|
||||||
content: finalPrompt,
|
|
||||||
conversacionId: activeChatId || undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentFields.length > 0) {
|
// Limpiar input inmediatamente para feedback visual
|
||||||
payload.campos = currentFields.map((f) => f.key)
|
setInput('')
|
||||||
|
setSelectedFields([])
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
planId,
|
||||||
|
content: buildPrompt(rawText, currentFields),
|
||||||
|
conversacionId: activeChatId,
|
||||||
|
campos:
|
||||||
|
currentFields.length > 0
|
||||||
|
? currentFields.map((f) => f.key)
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await sendChat(payload)
|
const response = await sendChat(payload)
|
||||||
|
|
||||||
|
// IMPORTANTE: Si es un chat nuevo, actualizar el ID antes de invalidar
|
||||||
if (response.conversacionId && response.conversacionId !== activeChatId) {
|
if (response.conversacionId && response.conversacionId !== activeChatId) {
|
||||||
setActiveChatId(response.conversacionId)
|
setActiveChatId(response.conversacionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
await queryClient.invalidateQueries({
|
// Invalidar ambas para asegurar que la lista de la izquierda y los mensajes se
|
||||||
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['conversation-by-plan', planId],
|
queryKey: ['conversation-by-plan', planId],
|
||||||
})
|
}),
|
||||||
setOptimisticMessage(null)
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['conversation-messages', response.conversacionId],
|
||||||
|
}),
|
||||||
|
])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error en el chat:', error)
|
console.error('Error:', error)
|
||||||
// Aquí sí podrías usar un toast o un mensaje de error temporal
|
|
||||||
} finally {
|
} finally {
|
||||||
// 5. CRÍTICO: Detener el estado de carga SIEMPRE
|
|
||||||
setIsSending(false)
|
setIsSending(false)
|
||||||
setOptimisticMessage(null)
|
setOptimisticMessage(null)
|
||||||
}
|
}
|
||||||
@@ -666,6 +683,7 @@ function RouteComponent() {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<ImprovementCard
|
<ImprovementCard
|
||||||
suggestions={msg.suggestions}
|
suggestions={msg.suggestions}
|
||||||
|
dbMessageId={msg.dbMessageId}
|
||||||
planId={planId}
|
planId={planId}
|
||||||
currentDatos={data?.datos}
|
currentDatos={data?.datos}
|
||||||
activeChatId={activeChatId}
|
activeChatId={activeChatId}
|
||||||
|
|||||||
Reference in New Issue
Block a user