Merge branch 'issue/136-que-la-conversacin-se-obtenga-de-conversationjson-'

This commit is contained in:
2026-02-25 14:03:30 -06:00
4 changed files with 121 additions and 116 deletions

View File

@@ -2,21 +2,25 @@ import { Check, Loader2 } from 'lucide-react'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { useUpdatePlanFields } from '@/data' // Tu hook existente
import { useUpdatePlanFields, useUpdateRecommendationApplied } from '@/data' // Tu hook existente
export const ImprovementCard = ({
suggestions,
onApply,
planId, // Necesitamos el ID
currentDatos, // Necesitamos los datos actuales para no sobrescribir todo el JSON
activeChatId,
}: {
suggestions: Array<any>
onApply?: (key: string, value: string) => void
planId: string
currentDatos: any
activeChatId: any
}) => {
const [appliedFields, setAppliedFields] = useState<Array<string>>([])
const [localApplied, setLocalApplied] = useState<Array<string>>([])
const updatePlan = useUpdatePlanFields()
const updateAppliedStatus = useUpdateRecommendationApplied()
const handleApply = (key: string, newValue: string) => {
if (!currentDatos) return
@@ -52,6 +56,14 @@ export const ImprovementCard = ({
setAppliedFields((prev) => [...prev, key])
if (onApply) onApply(key, newValue)
console.log(`Campo ${key} guardado exitosamente`)
if (activeChatId) {
updateAppliedStatus.mutate({
conversacionId: activeChatId,
campoAfectado: key,
})
}
if (onApply) onApply(key, newValue)
},
},
)
@@ -60,7 +72,7 @@ export const ImprovementCard = ({
return (
<div className="mt-2 flex w-full flex-col gap-4">
{suggestions.map((sug) => {
const isApplied = appliedFields.includes(sug.key)
const isApplied = sug.applied === true || localApplied.includes(sug.key)
const isUpdating =
updatePlan.isPending &&
updatePlan.variables.patch.datos?.[sug.key] !== undefined

View File

@@ -192,3 +192,48 @@ export async function update_conversation_title(
if (error) throw error
return data
}
export async function update_recommendation_applied_status(
conversacionId: string,
campoAfectado: string,
) {
const supabase = supabaseBrowser()
// 1. Obtener el estado actual del JSON
const { data: conv, error: fetchError } = await supabase
.from('conversaciones_plan')
.select('conversacion_json')
.eq('id', conversacionId)
.single()
if (fetchError) throw fetchError
if (!conv?.conversacion_json)
throw new Error('No se encontró la conversación')
// 2. Transformar el JSON para marcar como aplicada la recomendación específica
// Usamos una transformación inmutable para evitar efectos secundarios
const nuevoJson = (conv.conversacion_json as Array<any>).map((msg) => {
if (msg.user === 'assistant' && Array.isArray(msg.recommendations)) {
return {
...msg,
recommendations: msg.recommendations.map((rec: any) =>
rec.campo_afectado === campoAfectado
? { ...rec, aplicada: true }
: rec,
),
}
}
return msg
})
// 3. Actualizar la base de datos con el nuevo JSON
const { data, error: updateError } = await supabase
.from('conversaciones_plan')
.update({ conversacion_json: nuevoJson })
.eq('id', conversacionId)
.select()
.single()
if (updateError) throw updateError
return data
}

View File

@@ -10,6 +10,7 @@ import {
getConversationByPlan,
library_search,
update_conversation_status,
update_recommendation_applied_status,
update_conversation_title,
} from '../api/ai.api'
@@ -87,6 +88,31 @@ export function useConversationByPlan(planId: string | null) {
})
}
export function useUpdateRecommendationApplied() {
const qc = useQueryClient()
return useMutation({
mutationFn: ({
conversacionId,
campoAfectado,
}: {
conversacionId: string
campoAfectado: string
}) => update_recommendation_applied_status(conversacionId, campoAfectado),
onSuccess: (_, variables) => {
// Invalidamos la query para que useConversationByPlan refresque el JSON
qc.invalidateQueries({ queryKey: ['conversation-by-plan'] })
console.log(
`Recomendación ${variables.campoAfectado} marcada como aplicada.`,
)
},
onError: (error) => {
console.error('Error al actualizar el estado de la recomendación:', error)
},
})
}
export function useAISubjectImprove() {
return useMutation({ mutationFn: ai_subject_improve })
}

View File

@@ -27,7 +27,6 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { Textarea } from '@/components/ui/textarea'
import {
useAIPlanChat,
useChatHistory,
useConversationByPlan,
useUpdateConversationStatus,
useUpdateConversationTitle,
@@ -85,8 +84,9 @@ function RouteComponent() {
undefined,
)
const { data: historyMessages, isLoading: isLoadingHistory } =
useChatHistory(activeChatId)
/* const { data: historyMessages, isLoading: isLoadingHistory } =
useChatHistory(activeChatId) */
const { data: lastConversation, isLoading: isLoadingConv } =
useConversationByPlan(planId)
// archivos
@@ -120,58 +120,34 @@ function RouteComponent() {
}),
)
}, [data])
const activeChatData = useMemo(() => {
return lastConversation?.find((chat: any) => chat.id === activeChatId)
}, [lastConversation, activeChatId])
useEffect(() => {
// 1. Si no hay ID o está cargando el historial, no hacemos nada
if (!activeChatId || isLoadingHistory) return
const conversacionJson = activeChatData?.conversacion_json || []
const chatMessages = useMemo(() => {
const json = activeChatData?.conversacion_json
if (!Array.isArray(json)) return []
const messagesFromApi = historyMessages?.items || historyMessages
if (Array.isArray(messagesFromApi)) {
const flattened = messagesFromApi.map((msg) => {
let content = msg.content
let suggestions: Array<any> = []
if (typeof content === 'object' && content !== null) {
suggestions = Object.entries(content)
.filter(([key]) => key !== 'ai-message')
.map(([key, value]) => ({
key,
label: key.replace(/_/g, ' '),
newValue: value as string,
}))
content = content['ai-message'] || JSON.stringify(content)
}
// Si el content es un string que parece JSON (caso común en respuestas RAW)
else if (typeof content === 'string' && content.startsWith('{')) {
try {
const parsed = JSON.parse(content)
suggestions = Object.entries(parsed)
.filter(([key]) => key !== 'ai-message')
.map(([key, value]) => ({
key,
label: key.replace(/_/g, ' '),
newValue: value as string,
}))
content = parsed['ai-message'] || content
} catch (e) {
/* no es json */
}
}
return json.map((msg: any, index: number) => {
const isAssistant = msg.user === 'assistant'
return {
...msg,
content,
suggestions,
type: suggestions.length > 0 ? 'improvement-card' : 'text',
id: `${activeChatId}-${index}-${msg.timestamp}`, // ID estable
role: isAssistant ? 'assistant' : 'user',
content: isAssistant ? msg.message : msg.prompt,
suggestions:
isAssistant && msg.recommendations
? msg.recommendations.map((rec: any) => ({
key: rec.campo_afectado,
label: rec.campo_afectado.replace(/_/g, ' '),
newValue: rec.texto_mejora,
applied: rec.aplicada,
}))
: [],
}
})
if (!isLoading) {
setMessages(flattened.reverse())
}
}
}, [historyMessages, activeChatId, isLoadingHistory, isLoading])
}, [activeChatData, activeChatId])
useEffect(() => {
// Si no hay un chat seleccionado manualmente y la API nos devuelve chats existentes
@@ -309,13 +285,6 @@ function RouteComponent() {
const currentFields = [...selectedFields]
const finalPrompt = buildPrompt(rawText, currentFields)
const userMsg = {
id: Date.now().toString(),
role: 'user',
content: rawText,
}
setMessages((prev) => [...prev, userMsg])
setInput('')
try {
const payload: any = {
@@ -332,58 +301,14 @@ function RouteComponent() {
if (response.conversacionId && response.conversacionId !== activeChatId) {
setActiveChatId(response.conversacionId)
}
// Esto obliga a 'useConversationByPlan' a buscar en la DB el nuevo chat creado
queryClient.invalidateQueries({
await queryClient.invalidateQueries({
queryKey: ['conversation-by-plan', planId],
})
}
// --- NUEVA LÓGICA DE PARSEO ---
let aiText = 'Sin respuesta del asistente'
let suggestions: Array<any> = []
if (response.raw) {
try {
const rawData = JSON.parse(response.raw)
// Extraemos el mensaje conversacional
aiText = rawData['ai-message'] || 'Cambios aplicados con éxito.'
// Filtramos todo lo que no sea el mensaje para crear las sugerencias
suggestions = Object.entries(rawData)
.filter(([key]) => key !== 'ai-message')
.map(([key, value]) => ({
key,
label: key.replace(/_/g, ' '),
newValue: value as string,
}))
} catch (e) {
console.error('Error parseando el campo raw:', e)
aiText = response.raw // Fallback si no es JSON
}
}
setMessages((prev) => [
...prev,
{
id: Date.now().toString(),
role: 'assistant',
content: aiText,
type: suggestions.length > 0 ? 'improvement-card' : 'text',
suggestions: suggestions,
},
])
} catch (error) {
console.error('Error en el chat:', error)
setMessages((prev) => [
...prev,
{
id: 'error',
role: 'assistant',
content: 'Lo siento, hubo un error al procesar tu solicitud.',
},
])
// Aquí sí podrías usar un toast o un mensaje de error temporal
}
}
@@ -576,7 +501,7 @@ function RouteComponent() {
<div className="relative min-h-0 flex-1">
<ScrollArea ref={scrollRef} className="h-full w-full">
<div className="mx-auto max-w-3xl space-y-6 p-6">
{messages.map((msg) => (
{chatMessages.map((msg) => (
<div
key={msg.id}
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'ml-auto items-end' : 'items-start'}`}
@@ -596,14 +521,11 @@ function RouteComponent() {
<div className="mt-4">
<ImprovementCard
suggestions={msg.suggestions}
planId={planId} // Del useParams()
currentDatos={data?.datos} // De tu query usePlan(planId)
onApply={(key, val) => {
// Esto es opcional, si quieres hacer algo más en la UI del chat
console.log(
'Evento onApply disparado desde el chat',
)
}}
planId={planId}
currentDatos={data?.datos}
activeChatId={activeChatId}
// Puedes pasar una prop nueva si tu ImprovementCard la soporta:
// isReadOnly={msg.suggestions.every(s => s.applied)}
/>
</div>
)}