import { useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import CoachMessage from '../components/CoachMessage'
import UserMessage from '../components/UserMessage'
import QuickReplies from '../components/QuickReplies'
const API = '/api'
const initialData = {
name: '',
gender: '',
age: '',
height_cm: '',
weight: '',
neck_cm: '',
waist_cm: '',
hip_cm: '',
experience_level: '',
bench_1rm: '',
squat_1rm: '',
deadlift_1rm: '',
goal: '',
workouts_per_week: ''
}
const calcBodyFat = (gender, waist, neck, hip, height) => {
if (!waist || !neck || !height) return null
if (gender === 'female' && !hip) return null
if (gender === 'male') {
return Math.max(
0,
495 / (1.0324 - 0.19077 * Math.log10(waist - neck) + 0.15456 * Math.log10(height)) - 450
).toFixed(1)
}
return Math.max(
0,
495 / (1.29579 - 0.35004 * Math.log10(waist + hip - neck) + 0.221 * Math.log10(height)) - 450
).toFixed(1)
}
const toNumberOrNull = (value) => {
if (value === '' || value === null || value === undefined) return null
const numberValue = Number(value)
return Number.isNaN(numberValue) ? null : numberValue
}
export default function ChatOnboarding() {
const { token, updateProfile, refreshProfile } = useAuth()
const navigate = useNavigate()
const [data, setData] = useState(initialData)
const [messages, setMessages] = useState([])
const [currentIndex, setCurrentIndex] = useState(0)
const [answers, setAnswers] = useState([])
const [inputValue, setInputValue] = useState('')
const [error, setError] = useState('')
const [isTyping, setIsTyping] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const endRef = useRef(null)
const messageIdRef = useRef(0)
const typingTimeoutRef = useRef(null)
const questions = useMemo(() => {
return [
{
id: 'name',
field: 'name',
type: 'text',
prompt: 'Hej! Jag är din coach. Vad heter du?',
placeholder: 'Ditt namn',
inputType: 'text'
},
{
id: 'goal',
field: 'goal',
type: 'options',
prompt: values => `Kul att träffas${values.name ? ` ${values.name}` : ''}! Vad är ditt största mål?`,
options: [
{ label: 'Bygga muskler', value: 'muscle' },
{ label: 'Styrka', value: 'strength' },
{ label: 'Gå ner i vikt', value: 'fat_loss' },
{ label: 'Hälsa', value: 'general' }
]
},
{
id: 'experience_level',
field: 'experience_level',
type: 'options',
prompt: 'Hur länge har du tränat?',
options: [
{ label: 'Ny', value: 'beginner' },
{ label: '< 1 år', value: 'beginner' },
{ label: '1-3 år', value: 'intermediate' },
{ label: '3+ år', value: 'advanced' }
]
},
{
id: 'workouts_per_week',
field: 'workouts_per_week',
type: 'options',
prompt: 'Hur många pass kan du köra per vecka?',
options: [2, 3, 4, 5, 6].map(n => ({ label: `${n}`, value: n }))
},
{
id: 'gender',
field: 'gender',
type: 'options',
prompt: 'Super! Vi tar några snabba basfrågor. Vilket kön identifierar du dig som?',
options: [
{ label: 'Man', value: 'male' },
{ label: 'Kvinna', value: 'female' }
]
},
{
id: 'age',
field: 'age',
type: 'text',
prompt: 'Hur gammal är du?',
placeholder: 'Ålder',
inputType: 'number',
validate: value => (value > 0 && value < 120 ? '' : 'Skriv in en giltig ålder.')
},
{
id: 'height_cm',
field: 'height_cm',
type: 'text',
prompt: 'Hur lång är du? (cm)',
placeholder: '175',
inputType: 'number',
unit: 'cm',
validate: value => (value > 50 && value < 260 ? '' : 'Skriv in din längd i cm.')
},
{
id: 'weight',
field: 'weight',
type: 'text',
prompt: 'Vad väger du just nu? (kg)',
placeholder: '75',
inputType: 'number',
unit: 'kg',
validate: value => (value > 20 && value < 300 ? '' : 'Skriv in din vikt i kg.')
},
{
id: 'neck_cm',
field: 'neck_cm',
type: 'text',
prompt: 'Om du vet: halsmått i cm?',
placeholder: '38',
inputType: 'number',
unit: 'cm',
optional: true
},
{
id: 'waist_cm',
field: 'waist_cm',
type: 'text',
prompt: 'Midjemått i cm?',
placeholder: '85',
inputType: 'number',
unit: 'cm',
optional: true
},
{
id: 'hip_cm',
field: 'hip_cm',
type: 'text',
prompt: 'Höftmått i cm?',
placeholder: '95',
inputType: 'number',
unit: 'cm',
optional: true,
shouldAsk: values => values.gender === 'female'
},
{
id: 'bench_1rm',
field: 'bench_1rm',
type: 'text',
prompt: 'Har du en uppskattad 1RM i bänkpress? (valfritt, kg)',
placeholder: '100',
inputType: 'number',
unit: 'kg',
optional: true,
shouldAsk: values => values.experience_level && values.experience_level !== 'beginner'
},
{
id: 'squat_1rm',
field: 'squat_1rm',
type: 'text',
prompt: '1RM i knäböj? (valfritt, kg)',
placeholder: '140',
inputType: 'number',
unit: 'kg',
optional: true,
shouldAsk: values => values.experience_level && values.experience_level !== 'beginner'
},
{
id: 'deadlift_1rm',
field: 'deadlift_1rm',
type: 'text',
prompt: '1RM i marklyft? (valfritt, kg)',
placeholder: '160',
inputType: 'number',
unit: 'kg',
optional: true,
shouldAsk: values => values.experience_level && values.experience_level !== 'beginner'
}
]
}, [])
const currentQuestion = questions[currentIndex]
const addMessage = (message) => {
messageIdRef.current += 1
setMessages(prev => [...prev, { id: messageIdRef.current, ...message }])
}
const scrollToBottom = () => {
if (endRef.current) {
endRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' })
}
}
useEffect(() => {
scrollToBottom()
}, [messages, isTyping])
const getPrompt = (question, values) => {
if (!question) return ''
return typeof question.prompt === 'function' ? question.prompt(values) : question.prompt
}
useEffect(() => {
if (messages.length === 0 && currentQuestion) {
addMessage({ sender: 'coach', text: getPrompt(currentQuestion, data), questionIndex: currentIndex })
}
return () => {
if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current)
}
}, [messages.length, currentQuestion, currentIndex, data])
const applyAnswer = (values, question, value) => {
if (!question.field) return values
return { ...values, [question.field]: value }
}
const rebuildDataFromAnswers = (updatedAnswers) => {
return updatedAnswers.reduce((acc, answer) => {
const question = questions[answer.questionIndex]
if (!question) return acc
return applyAnswer(acc, question, answer.value)
}, { ...initialData })
}
const findNextIndex = (startIndex, nextData) => {
for (let i = startIndex + 1; i < questions.length; i += 1) {
const question = questions[i]
if (!question?.shouldAsk || question.shouldAsk(nextData)) return i
}
return null
}
const isMeasurementQuestion = (questionId) => ['weight', 'neck_cm', 'waist_cm', 'hip_cm'].includes(questionId)
const isStrengthQuestion = (questionId) => ['bench_1rm', 'squat_1rm', 'deadlift_1rm'].includes(questionId)
const isProfileQuestion = (questionId) => ['gender', 'age', 'height_cm', 'experience_level', 'goal', 'workouts_per_week'].includes(questionId)
const saveProfile = async (values, complete = false) => {
const payload = {
gender: values.gender || null,
age: toNumberOrNull(values.age),
height_cm: toNumberOrNull(values.height_cm),
experience_level: values.experience_level || null,
goal: values.goal || null,
workouts_per_week: toNumberOrNull(values.workouts_per_week),
onboarding_complete: complete
}
await updateProfile(payload)
}
const saveMeasurements = async (values) => {
const bodyFat = calcBodyFat(
values.gender,
toNumberOrNull(values.waist_cm),
toNumberOrNull(values.neck_cm),
toNumberOrNull(values.hip_cm),
toNumberOrNull(values.height_cm)
)
if (!values.weight && !values.neck_cm && !values.waist_cm && !values.hip_cm) return
await fetch(`${API}/user/measurements`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({
weight: toNumberOrNull(values.weight),
neck_cm: toNumberOrNull(values.neck_cm),
waist_cm: toNumberOrNull(values.waist_cm),
hip_cm: toNumberOrNull(values.hip_cm),
body_fat_pct: bodyFat
})
})
}
const saveStrength = async (values) => {
if (!values.bench_1rm && !values.squat_1rm && !values.deadlift_1rm) return
await fetch(`${API}/user/strength`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({
bench_1rm: toNumberOrNull(values.bench_1rm),
squat_1rm: toNumberOrNull(values.squat_1rm),
deadlift_1rm: toNumberOrNull(values.deadlift_1rm)
})
})
}
const maybeAutoSave = async (questionId, nextData, nextIndex) => {
if (isProfileQuestion(questionId)) {
await saveProfile(nextData, false)
}
if (isMeasurementQuestion(questionId)) {
const nextQuestion = nextIndex !== null ? questions[nextIndex] : null
if (!nextQuestion || !isMeasurementQuestion(nextQuestion.id)) {
await saveMeasurements(nextData)
}
}
if (isStrengthQuestion(questionId)) {
const nextQuestion = nextIndex !== null ? questions[nextIndex] : null
if (!nextQuestion || !isStrengthQuestion(nextQuestion.id)) {
await saveStrength(nextData)
}
}
}
const handleAnswer = async (answerValue, answerLabel = null) => {
if (!currentQuestion) return
const label = answerLabel ?? `${answerValue}`
setError('')
const nextData = applyAnswer(data, currentQuestion, answerValue)
const nextIndex = findNextIndex(currentIndex, nextData)
addMessage({ sender: 'user', text: label, questionIndex: currentIndex })
setAnswers(prev => [...prev, { questionIndex: currentIndex, value: answerValue, label }])
setData(nextData)
setInputValue('')
try {
await maybeAutoSave(currentQuestion.id, nextData, nextIndex)
} catch (err) {
console.error('Autosave error:', err)
}
if (nextIndex === null) {
setIsTyping(true)
typingTimeoutRef.current = setTimeout(async () => {
addMessage({
sender: 'coach',
text: 'Perfekt! Jag har allt jag behöver. Låt mig bygga ditt program...',
questionIndex: currentIndex + 1
})
setIsTyping(false)
await finishOnboarding(nextData)
}, 700)
return
}
setIsTyping(true)
typingTimeoutRef.current = setTimeout(() => {
addMessage({ sender: 'coach', text: getPrompt(questions[nextIndex], nextData), questionIndex: nextIndex })
setCurrentIndex(nextIndex)
setIsTyping(false)
}, 600)
}
const finishOnboarding = async (values) => {
setIsSaving(true)
try {
await saveProfile(values, true)
await saveMeasurements(values)
await saveStrength(values)
if (refreshProfile) await refreshProfile()
navigate('/')
} catch (err) {
console.error('Onboarding error:', err)
setIsSaving(false)
}
}
const handleTextSubmit = () => {
if (!currentQuestion) return
const raw = inputValue.trim()
if (!raw && !currentQuestion.optional) {
setError('Skriv ett svar för att gå vidare.')
return
}
let numericValue = raw
if (currentQuestion.inputType === 'number' && raw) {
const parsed = Number(raw)
if (Number.isNaN(parsed)) {
setError('Skriv ett giltigt nummer.')
return
}
const validationMessage = currentQuestion.validate ? currentQuestion.validate(parsed) : ''
if (validationMessage) {
setError(validationMessage)
return
}
numericValue = parsed
}
if (!raw && currentQuestion.optional) {
handleAnswer('', 'Hoppar')
return
}
const label = currentQuestion.unit ? `${raw} ${currentQuestion.unit}` : raw
handleAnswer(numericValue, label)
}
const handleQuickReply = (option) => {
if (option.action === 'back') {
handleBack()
return
}
if (option.action === 'skip') {
handleAnswer('', 'Hoppar')
return
}
handleAnswer(option.value, option.label)
}
const handleBack = () => {
if (!answers.length) return
const lastAnswer = answers[answers.length - 1]
const targetIndex = lastAnswer.questionIndex
const trimmedMessages = [...messages]
const lastCoachIndex = trimmedMessages
.map((msg, idx) => (msg.sender === 'coach' && msg.questionIndex === targetIndex ? idx : -1))
.filter(idx => idx !== -1)
.pop()
if (lastCoachIndex !== undefined) {
trimmedMessages.splice(lastCoachIndex + 1)
}
const updatedAnswers = answers.slice(0, -1)
setAnswers(updatedAnswers)
setMessages(trimmedMessages)
setCurrentIndex(targetIndex)
setData(rebuildDataFromAnswers(updatedAnswers))
setIsTyping(false)
setInputValue('')
setError('')
}
const renderInputArea = () => {
if (!currentQuestion) return null
if (currentQuestion.type === 'options') {
const options = [...currentQuestion.options]
const actionOptions = []
if (currentQuestion.optional) {
actionOptions.push({ label: 'Hoppa över', action: 'skip', variant: 'ghost' })
}
if (answers.length) {
actionOptions.push({ label: 'Ändra senaste', action: 'back', variant: 'ghost' })
}
return (
<>
Coach