Inspección nueva, Pregunta 1 (Frenos): Abro chat → aiChatMessages = [] (vacío) Pregunto sobre frenos Cierro → se guarda en answers[pregunta1].chatHistory Misma inspección, Pregunta 2 (Neumáticos): Abro chat → aiChatMessages = [] (vacío, porque esta pregunta no tiene historial) Chat limpio, sin mezclar con frenos ✅ Vuelvo a Pregunta 1: Abro chat → aiChatMessages = [historial guardado] Veo mi conversación anterior sobre frenos ✅ Nueva inspección en otro momento: Todas las preguntas empiezan con aiChatMessages = [] No se mezcla con inspecciones anteriores ✅
333 lines
10 KiB
JavaScript
333 lines
10 KiB
JavaScript
import React from 'react'
|
||
|
||
/**
|
||
* Renderizador Dinámico de Campos de Respuesta
|
||
* Renderiza el input apropiado según la configuración de la pregunta
|
||
*/
|
||
export function QuestionAnswerInput({ question, value, onChange, onSave }) {
|
||
const config = question.options || {}
|
||
const questionType = config.type || question.type
|
||
|
||
// BOOLEAN (2 opciones)
|
||
if (questionType === 'boolean' && config.choices?.length === 2) {
|
||
const [choice1, choice2] = config.choices
|
||
|
||
return (
|
||
<div className="flex gap-4">
|
||
<label className="flex items-center cursor-pointer px-4 py-3 border-2 rounded-lg transition hover:bg-gray-50">
|
||
<input
|
||
type="radio"
|
||
value={choice1.value}
|
||
checked={value === choice1.value}
|
||
onChange={(e) => {
|
||
onChange(e.target.value)
|
||
onSave?.()
|
||
}}
|
||
className="mr-3"
|
||
/>
|
||
<span className={`font-medium ${choice1.status === 'ok' ? 'text-green-600' : 'text-red-600'}`}>
|
||
{choice1.status === 'ok' ? '✓' : '✗'} {choice1.label}
|
||
</span>
|
||
</label>
|
||
<label className="flex items-center cursor-pointer px-4 py-3 border-2 rounded-lg transition hover:bg-gray-50">
|
||
<input
|
||
type="radio"
|
||
value={choice2.value}
|
||
checked={value === choice2.value}
|
||
onChange={(e) => {
|
||
onChange(e.target.value)
|
||
onSave?.()
|
||
}}
|
||
className="mr-3"
|
||
/>
|
||
<span className={`font-medium ${choice2.status === 'ok' ? 'text-green-600' : 'text-red-600'}`}>
|
||
{choice2.status === 'ok' ? '✓' : '✗'} {choice2.label}
|
||
</span>
|
||
</label>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// SINGLE CHOICE (selección única)
|
||
if (questionType === 'single_choice' && config.choices) {
|
||
return (
|
||
<div className="space-y-2">
|
||
{config.choices.map((choice, idx) => (
|
||
<label
|
||
key={idx}
|
||
className="flex items-center cursor-pointer px-4 py-3 border-2 rounded-lg transition hover:bg-gray-50"
|
||
>
|
||
<input
|
||
type="radio"
|
||
value={choice.value}
|
||
checked={value === choice.value}
|
||
onChange={(e) => {
|
||
onChange(e.target.value)
|
||
onSave?.()
|
||
}}
|
||
className="mr-3"
|
||
/>
|
||
<span className="flex-1 font-medium">{choice.label}</span>
|
||
{choice.points > 0 && (
|
||
<span className="text-sm text-blue-600">+{choice.points} pts</span>
|
||
)}
|
||
</label>
|
||
))}
|
||
|
||
{config.allow_other && (
|
||
<div className="pl-7">
|
||
<label className="flex items-center">
|
||
<input
|
||
type="radio"
|
||
value="__other__"
|
||
checked={value && !config.choices.find(c => c.value === value)}
|
||
onChange={(e) => onChange('')}
|
||
className="mr-3"
|
||
/>
|
||
<span>Otro:</span>
|
||
<input
|
||
type="text"
|
||
value={value && !config.choices.find(c => c.value === value) ? value : ''}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
onBlur={onSave}
|
||
className="ml-2 flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||
placeholder="Especificar..."
|
||
/>
|
||
</label>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// MULTIPLE CHOICE (selección múltiple)
|
||
if (questionType === 'multiple_choice' && config.choices) {
|
||
const selectedValues = value ? (Array.isArray(value) ? value : value.split(',')) : []
|
||
|
||
const handleToggle = (choiceValue) => {
|
||
let newValues
|
||
if (selectedValues.includes(choiceValue)) {
|
||
newValues = selectedValues.filter(v => v !== choiceValue)
|
||
} else {
|
||
newValues = [...selectedValues, choiceValue]
|
||
}
|
||
onChange(newValues.join(','))
|
||
onSave?.()
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-2">
|
||
{config.choices.map((choice, idx) => (
|
||
<label
|
||
key={idx}
|
||
className="flex items-center cursor-pointer px-4 py-3 border-2 rounded-lg transition hover:bg-gray-50"
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedValues.includes(choice.value)}
|
||
onChange={() => handleToggle(choice.value)}
|
||
className="mr-3 w-4 h-4"
|
||
/>
|
||
<span className="flex-1 font-medium">{choice.label}</span>
|
||
{choice.points > 0 && (
|
||
<span className="text-sm text-blue-600">+{choice.points} pts</span>
|
||
)}
|
||
</label>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// SCALE (escala numérica)
|
||
if (questionType === 'scale') {
|
||
const min = config.min || 1
|
||
const max = config.max || 5
|
||
const step = config.step || 1
|
||
const labels = config.labels || {}
|
||
|
||
const options = []
|
||
for (let i = min; i <= max; i += step) {
|
||
options.push(i)
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
<div className="flex justify-between text-sm text-gray-600 mb-2">
|
||
{labels.min && <span>{labels.min}</span>}
|
||
{labels.max && <span>{labels.max}</span>}
|
||
</div>
|
||
<div className="flex gap-2 justify-center">
|
||
{options.map(num => (
|
||
<button
|
||
key={num}
|
||
type="button"
|
||
onClick={() => {
|
||
onChange(String(num))
|
||
onSave?.()
|
||
}}
|
||
className={`w-12 h-12 rounded-full font-bold transition ${
|
||
value === String(num)
|
||
? 'bg-blue-600 text-white scale-110'
|
||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||
}`}
|
||
>
|
||
{num}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="text-center">
|
||
{value && (
|
||
<div className="inline-flex items-center gap-2 px-4 py-2 bg-blue-50 rounded-lg">
|
||
<span className="text-sm text-gray-600">Seleccionado:</span>
|
||
<span className="font-bold text-blue-600 text-lg">{value}</span>
|
||
<span className="text-sm text-gray-600">/ {max}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// TEXT (texto libre)
|
||
if (questionType === 'text') {
|
||
const multiline = config.multiline !== false
|
||
const maxLength = config.max_length || 500
|
||
|
||
if (multiline) {
|
||
return (
|
||
<div>
|
||
<textarea
|
||
value={value || ''}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
onBlur={onSave}
|
||
maxLength={maxLength}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||
rows="4"
|
||
placeholder="Ingrese su respuesta..."
|
||
/>
|
||
<div className="text-xs text-gray-500 mt-1 text-right">
|
||
{(value?.length || 0)} / {maxLength} caracteres
|
||
</div>
|
||
</div>
|
||
)
|
||
} else {
|
||
return (
|
||
<input
|
||
type="text"
|
||
value={value || ''}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
onBlur={onSave}
|
||
maxLength={maxLength}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||
placeholder="Ingrese su respuesta..."
|
||
/>
|
||
)
|
||
}
|
||
}
|
||
|
||
// NUMBER (valor numérico)
|
||
if (questionType === 'number') {
|
||
const min = config.min ?? 0
|
||
const max = config.max ?? 100
|
||
const unit = config.unit || ''
|
||
|
||
return (
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type="number"
|
||
value={value || ''}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
onBlur={onSave}
|
||
min={min}
|
||
max={max}
|
||
step="any"
|
||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||
placeholder={`${min} - ${max}`}
|
||
/>
|
||
{unit && <span className="text-gray-600 font-medium">{unit}</span>}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// DATE (fecha)
|
||
if (questionType === 'date') {
|
||
return (
|
||
<input
|
||
type="date"
|
||
value={value || ''}
|
||
onChange={(e) => {
|
||
onChange(e.target.value)
|
||
onSave?.()
|
||
}}
|
||
min={config.min_date}
|
||
max={config.max_date}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
)
|
||
}
|
||
|
||
// TIME (hora)
|
||
if (questionType === 'time') {
|
||
return (
|
||
<input
|
||
type="time"
|
||
value={value || ''}
|
||
onChange={(e) => {
|
||
onChange(e.target.value)
|
||
onSave?.()
|
||
}}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
)
|
||
}
|
||
|
||
// PHOTO_ONLY (solo foto, sin campo de respuesta)
|
||
if (questionType === 'photo_only') {
|
||
return (
|
||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||
<p className="text-sm text-blue-800">
|
||
📸 Esta pregunta solo requiere fotografías. Adjunta las imágenes en la sección de fotos abajo.
|
||
</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// AI_ASSISTANT (Chat con asistente)
|
||
if (questionType === 'ai_assistant') {
|
||
return (
|
||
<div className="p-4 bg-gradient-to-r from-purple-50 to-blue-50 border-2 border-purple-200 rounded-lg">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<span className="text-2xl">💬</span>
|
||
<h4 className="font-semibold text-purple-900">Asistente Disponible</h4>
|
||
</div>
|
||
<p className="text-sm text-purple-700 mb-2">
|
||
Haz clic en el botón "💬 Consultar Asistente" debajo para abrir el chat.
|
||
El asistente ha analizado las fotos anteriores y está listo para ayudarte.
|
||
</p>
|
||
<div className="text-xs text-purple-600 bg-white/50 rounded px-2 py-1">
|
||
ℹ️ No requiere respuesta manual - el chat se guarda automáticamente
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Fallback para tipos desconocidos
|
||
return (
|
||
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||
<p className="text-sm text-yellow-800">
|
||
⚠️ Tipo de pregunta no reconocido: <code>{questionType}</code>
|
||
</p>
|
||
<input
|
||
type="text"
|
||
value={value || ''}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
onBlur={onSave}
|
||
className="mt-2 w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||
placeholder="Respuesta de texto libre..."
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default QuestionAnswerInput
|