Que la conversación se obtenga de conversation_json de supabase #136
This commit is contained in:
@@ -2,21 +2,25 @@ import { Check, Loader2 } from 'lucide-react'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useUpdatePlanFields } from '@/data' // Tu hook existente
|
import { useUpdatePlanFields, useUpdateRecommendationApplied } from '@/data' // Tu hook existente
|
||||||
|
|
||||||
export const ImprovementCard = ({
|
export const ImprovementCard = ({
|
||||||
suggestions,
|
suggestions,
|
||||||
onApply,
|
onApply,
|
||||||
planId, // Necesitamos el ID
|
planId, // Necesitamos el ID
|
||||||
currentDatos, // Necesitamos los datos actuales para no sobrescribir todo el JSON
|
currentDatos, // Necesitamos los datos actuales para no sobrescribir todo el JSON
|
||||||
|
activeChatId,
|
||||||
}: {
|
}: {
|
||||||
suggestions: Array<any>
|
suggestions: Array<any>
|
||||||
onApply?: (key: string, value: string) => void
|
onApply?: (key: string, value: string) => void
|
||||||
planId: string
|
planId: string
|
||||||
currentDatos: any
|
currentDatos: any
|
||||||
|
activeChatId: any
|
||||||
}) => {
|
}) => {
|
||||||
const [appliedFields, setAppliedFields] = useState<Array<string>>([])
|
const [appliedFields, setAppliedFields] = useState<Array<string>>([])
|
||||||
|
const [localApplied, setLocalApplied] = useState<Array<string>>([])
|
||||||
const updatePlan = useUpdatePlanFields()
|
const updatePlan = useUpdatePlanFields()
|
||||||
|
const updateAppliedStatus = useUpdateRecommendationApplied()
|
||||||
|
|
||||||
const handleApply = (key: string, newValue: string) => {
|
const handleApply = (key: string, newValue: string) => {
|
||||||
if (!currentDatos) return
|
if (!currentDatos) return
|
||||||
@@ -52,6 +56,14 @@ export const ImprovementCard = ({
|
|||||||
setAppliedFields((prev) => [...prev, key])
|
setAppliedFields((prev) => [...prev, key])
|
||||||
if (onApply) onApply(key, newValue)
|
if (onApply) onApply(key, newValue)
|
||||||
console.log(`Campo ${key} guardado exitosamente`)
|
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 (
|
return (
|
||||||
<div className="mt-2 flex w-full flex-col gap-4">
|
<div className="mt-2 flex w-full flex-col gap-4">
|
||||||
{suggestions.map((sug) => {
|
{suggestions.map((sug) => {
|
||||||
const isApplied = appliedFields.includes(sug.key)
|
const isApplied = sug.applied === true || localApplied.includes(sug.key)
|
||||||
const isUpdating =
|
const isUpdating =
|
||||||
updatePlan.isPending &&
|
updatePlan.isPending &&
|
||||||
updatePlan.variables.patch.datos?.[sug.key] !== undefined
|
updatePlan.variables.patch.datos?.[sug.key] !== undefined
|
||||||
|
|||||||
@@ -181,3 +181,65 @@ 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 update_conversation_title(
|
||||||
|
conversacionId: string,
|
||||||
|
nuevoTitulo: string,
|
||||||
|
) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('conversaciones_plan')
|
||||||
|
.update({ nombre: nuevoTitulo }) // Asegúrate que la columna se llame 'title' o 'nombre'
|
||||||
|
.eq('id', conversacionId)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getConversationByPlan,
|
getConversationByPlan,
|
||||||
library_search,
|
library_search,
|
||||||
update_conversation_status,
|
update_conversation_status,
|
||||||
|
update_recommendation_applied_status,
|
||||||
} from '../api/ai.api'
|
} from '../api/ai.api'
|
||||||
|
|
||||||
// eslint-disable-next-line node/prefer-node-protocol
|
// eslint-disable-next-line node/prefer-node-protocol
|
||||||
@@ -91,6 +92,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() {
|
export function useAISubjectImprove() {
|
||||||
return useMutation({ mutationFn: ai_subject_improve })
|
return useMutation({ mutationFn: ai_subject_improve })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import { ScrollArea } from '@/components/ui/scroll-area'
|
|||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import {
|
import {
|
||||||
useAIPlanChat,
|
useAIPlanChat,
|
||||||
useChatHistory,
|
|
||||||
useConversationByPlan,
|
useConversationByPlan,
|
||||||
useUpdateConversationStatus,
|
useUpdateConversationStatus,
|
||||||
} from '@/data'
|
} from '@/data'
|
||||||
@@ -84,8 +83,9 @@ function RouteComponent() {
|
|||||||
undefined,
|
undefined,
|
||||||
)
|
)
|
||||||
|
|
||||||
const { data: historyMessages, isLoading: isLoadingHistory } =
|
/* const { data: historyMessages, isLoading: isLoadingHistory } =
|
||||||
useChatHistory(activeChatId)
|
useChatHistory(activeChatId) */
|
||||||
|
|
||||||
const { data: lastConversation, isLoading: isLoadingConv } =
|
const { data: lastConversation, isLoading: isLoadingConv } =
|
||||||
useConversationByPlan(planId)
|
useConversationByPlan(planId)
|
||||||
// archivos
|
// archivos
|
||||||
@@ -106,61 +106,48 @@ function RouteComponent() {
|
|||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const [showArchived, setShowArchived] = useState(false)
|
const [showArchived, setShowArchived] = useState(false)
|
||||||
|
const [editingChatId, setEditingChatId] = useState<string | null>(null)
|
||||||
|
const editableRef = useRef<HTMLSpanElement>(null)
|
||||||
|
const { mutate: updateTitleMutation } = useUpdateConversationTitle()
|
||||||
|
|
||||||
useEffect(() => {
|
const availableFields = useMemo(() => {
|
||||||
// 1. Si no hay ID o está cargando el historial, no hacemos nada
|
if (!data?.estructuras_plan?.definicion?.properties) return []
|
||||||
if (!activeChatId || isLoadingHistory) return
|
return Object.entries(data.estructuras_plan.definicion.properties).map(
|
||||||
|
([key, value]) => ({
|
||||||
|
key,
|
||||||
|
label: value.title,
|
||||||
|
value: String(value.description || ''),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}, [data])
|
||||||
|
const activeChatData = useMemo(() => {
|
||||||
|
return lastConversation?.find((chat: any) => chat.id === activeChatId)
|
||||||
|
}, [lastConversation, activeChatId])
|
||||||
|
|
||||||
const messagesFromApi = historyMessages?.items || historyMessages
|
const conversacionJson = activeChatData?.conversacion_json || []
|
||||||
|
const chatMessages = useMemo(() => {
|
||||||
|
const json = activeChatData?.conversacion_json
|
||||||
|
if (!Array.isArray(json)) return []
|
||||||
|
|
||||||
if (Array.isArray(messagesFromApi)) {
|
return json.map((msg: any, index: number) => {
|
||||||
const flattened = messagesFromApi.map((msg) => {
|
const isAssistant = msg.user === 'assistant'
|
||||||
let content = msg.content
|
|
||||||
let suggestions: Array<any> = []
|
|
||||||
|
|
||||||
if (typeof content === 'object' && content !== null) {
|
return {
|
||||||
suggestions = Object.entries(content)
|
id: `${activeChatId}-${index}-${msg.timestamp}`, // ID estable
|
||||||
.filter(([key]) => key !== 'ai-message')
|
role: isAssistant ? 'assistant' : 'user',
|
||||||
.map(([key, value]) => ({
|
content: isAssistant ? msg.message : msg.prompt,
|
||||||
key,
|
suggestions:
|
||||||
label: key.replace(/_/g, ' '),
|
isAssistant && msg.recommendations
|
||||||
newValue: value as string,
|
? msg.recommendations.map((rec: any) => ({
|
||||||
}))
|
key: rec.campo_afectado,
|
||||||
|
label: rec.campo_afectado.replace(/_/g, ' '),
|
||||||
content = content['ai-message'] || JSON.stringify(content)
|
newValue: rec.texto_mejora,
|
||||||
}
|
applied: rec.aplicada,
|
||||||
// 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 {
|
|
||||||
...msg,
|
|
||||||
content,
|
|
||||||
suggestions,
|
|
||||||
type: suggestions.length > 0 ? 'improvement-card' : 'text',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Solo actualizamos si no estamos esperando la respuesta de un POST
|
|
||||||
// para evitar saltos visuales
|
|
||||||
if (!isLoading) {
|
|
||||||
setMessages(flattened.reverse())
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}, [historyMessages, activeChatId, isLoadingHistory, isLoading])
|
}, [activeChatData, activeChatId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Si no hay un chat seleccionado manualmente y la API nos devuelve chats existentes
|
// Si no hay un chat seleccionado manualmente y la API nos devuelve chats existentes
|
||||||
@@ -321,13 +308,6 @@ function RouteComponent() {
|
|||||||
const currentFields = [...selectedFields]
|
const currentFields = [...selectedFields]
|
||||||
const finalPrompt = buildPrompt(rawText, currentFields)
|
const finalPrompt = buildPrompt(rawText, currentFields)
|
||||||
|
|
||||||
const userMsg = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
role: 'user',
|
|
||||||
content: rawText,
|
|
||||||
}
|
|
||||||
|
|
||||||
setMessages((prev) => [...prev, userMsg])
|
|
||||||
setInput('')
|
setInput('')
|
||||||
// setSelectedFields([])
|
// setSelectedFields([])
|
||||||
|
|
||||||
@@ -346,58 +326,14 @@ function RouteComponent() {
|
|||||||
|
|
||||||
if (response.conversacionId && response.conversacionId !== activeChatId) {
|
if (response.conversacionId && response.conversacionId !== activeChatId) {
|
||||||
setActiveChatId(response.conversacionId)
|
setActiveChatId(response.conversacionId)
|
||||||
|
|
||||||
// Esto obliga a 'useConversationByPlan' a buscar en la DB el nuevo chat creado
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ['conversation-by-plan', planId],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NUEVA LÓGICA DE PARSEO ---
|
await queryClient.invalidateQueries({
|
||||||
let aiText = 'Sin respuesta del asistente'
|
queryKey: ['conversation-by-plan', planId],
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Error en el chat:', error)
|
console.error('Error en el chat:', error)
|
||||||
setMessages((prev) => [
|
// Aquí sí podrías usar un toast o un mensaje de error temporal
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: 'error',
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'Lo siento, hubo un error al procesar tu solicitud.',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,7 +479,7 @@ function RouteComponent() {
|
|||||||
<div className="relative min-h-0 flex-1">
|
<div className="relative min-h-0 flex-1">
|
||||||
<ScrollArea ref={scrollRef} className="h-full w-full">
|
<ScrollArea ref={scrollRef} className="h-full w-full">
|
||||||
<div className="mx-auto max-w-3xl space-y-6 p-6">
|
<div className="mx-auto max-w-3xl space-y-6 p-6">
|
||||||
{messages.map((msg) => (
|
{chatMessages.map((msg) => (
|
||||||
<div
|
<div
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'ml-auto items-end' : 'items-start'}`}
|
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'ml-auto items-end' : 'items-start'}`}
|
||||||
@@ -563,14 +499,11 @@ function RouteComponent() {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<ImprovementCard
|
<ImprovementCard
|
||||||
suggestions={msg.suggestions}
|
suggestions={msg.suggestions}
|
||||||
planId={planId} // Del useParams()
|
planId={planId}
|
||||||
currentDatos={data?.datos} // De tu query usePlan(planId)
|
currentDatos={data?.datos}
|
||||||
onApply={(key, val) => {
|
activeChatId={activeChatId}
|
||||||
// Esto es opcional, si quieres hacer algo más en la UI del chat
|
// Puedes pasar una prop nueva si tu ImprovementCard la soporta:
|
||||||
console.log(
|
// isReadOnly={msg.suggestions.every(s => s.applied)}
|
||||||
'Evento onApply disparado desde el chat',
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user