Spaces:
Sleeping
Sleeping
| const messagesEl = document.getElementById('messages'); | |
| const inputArea = document.getElementById('input-area'); | |
| const balanceBar = document.getElementById('balance-bar'); | |
| const balanceValue = document.getElementById('balance-value'); | |
| const balanceProgress = document.getElementById('balance-progress'); | |
| const sendBtn = document.getElementById('send'); | |
| let state = { expenses: { fixed: {}, optional: {}, custom: {} } }; | |
| let queue = [ | |
| { key:'name', text:'Hello there! What’s your name?', type:'text' }, | |
| { key:'intro', text:"I’m your helpful advisor. I’ll help you make your money boom 💰—but remember, there’s always risk. Think twice!", type:'info' }, | |
| { key:'salary', text:'Enter your total monthly in-hand salary: ₹', type:'number' }, | |
| ...[ | |
| 'Rent/Mortgage','Groceries','Utilities','EMI/Loan' | |
| ].map(k=>({ key:k, text:`Enter your monthly ${k}: ₹`, type:'number', cat:'fixed' })), | |
| ...[ | |
| 'Insurance','Transport','Gym','Subs' | |
| ].flatMap(k=>[ | |
| { key:`has_${k}`, text:`Do you have ${k}? (y/n)`, type:'yesno' }, | |
| { key:k, text:`Enter your monthly ${k}: ₹`, type:'number', depends:`has_${k}`, cat:'optional' } | |
| ]), | |
| { key:'has_custom', text:'Any other recurring expenses to add? (y/n)', type:'yesno' }, | |
| { key:'custom_count', text:'How many?', type:'number', depends:'has_custom' }, | |
| // custom entries dynamic | |
| ]; | |
| let idx = 0, customCount = 0, customIdx = 0, awaitingCustom = false; | |
| // Helper: Render input control based on question type | |
| function renderInput(q) { | |
| inputArea.innerHTML = ''; | |
| let el; | |
| if(q.type==='yesno') { | |
| el = document.createElement('div'); | |
| ['Yes','No'].forEach(opt => { | |
| const btn = document.createElement('button'); | |
| btn.textContent = opt; | |
| btn.className = 'px-4 py-2 rounded-lg border bg-gray-50 hover:bg-blue-100 text-blue-700 font-semibold mx-1'; | |
| btn.onclick = () => { | |
| inputArea.querySelectorAll('button').forEach(b=>b.disabled=true); | |
| handleYesNo(opt); | |
| }; | |
| el.appendChild(btn); | |
| }); | |
| } else if(q.type==='number') { | |
| el = document.createElement('input'); | |
| el.type = 'number'; | |
| el.className = 'flex-1 p-3 rounded-lg border bg-gray-100 focus:outline-none'; | |
| el.placeholder = 'Enter amount'; | |
| el.id = 'input'; | |
| el.oninput = () => sendBtn.disabled = !el.value; | |
| } else { | |
| el = document.createElement('input'); | |
| el.type = 'text'; | |
| el.className = 'flex-1 p-3 rounded-lg border bg-gray-100 focus:outline-none'; | |
| el.placeholder = 'Type your answer...'; | |
| el.id = 'input'; | |
| el.oninput = () => sendBtn.disabled = !el.value; | |
| } | |
| inputArea.appendChild(el); | |
| if(q.type!=='yesno') { | |
| sendBtn.disabled = true; | |
| el.addEventListener('keydown', e => { if(e.key==='Enter' && el.value) sendBtn.click(); }); | |
| el.focus(); | |
| } else { | |
| sendBtn.disabled = true; | |
| } | |
| } | |
| // Helper: Update balance bar | |
| function updateBalanceBar(balance, salary) { | |
| if(salary && salary > 0) { | |
| balanceBar.classList.remove('hidden'); | |
| balanceValue.textContent = `₹${balance.toLocaleString()}`; | |
| let percent = Math.max(0, Math.min(100, Math.round((balance/salary)*100))); | |
| balanceProgress.style.width = percent + '%'; | |
| balanceProgress.className = | |
| 'h-2 rounded-full ' + (percent > 60 ? 'bg-green-400' : percent > 30 ? 'bg-yellow-400' : 'bg-red-400'); | |
| } else { | |
| balanceBar.classList.add('hidden'); | |
| } | |
| } | |
| // Helper: Add message to chat (supports markdown for bot) | |
| function addMessage(txt, who){ | |
| const div = document.createElement('div'); | |
| div.className = 'message flex items-end gap-2 ' + (who==='bot' | |
| ? 'justify-start' : 'justify-end flex-row-reverse'); | |
| if(who==='bot') { | |
| // Render markdown for bot replies | |
| const avatar = `<img src="https://img.icons8.com/color/48/000000/bot.png" class="w-8 h-8 rounded-full border shadow bg-white">`; | |
| const msg = document.createElement('div'); | |
| msg.className = 'bg-gray-100 text-gray-800 p-3 rounded-2xl max-w-[80%] whitespace-pre-line shadow-sm'; | |
| msg.innerHTML = marked.parse(txt) + `<div class='text-xs text-gray-400 mt-1 text-right'>${new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}</div>`; | |
| div.innerHTML = avatar; | |
| div.appendChild(msg); | |
| } else { | |
| const avatar = `<img src="https://img.icons8.com/color/48/000000/user-male-circle.png" class="w-8 h-8 rounded-full border shadow bg-blue-100">`; | |
| const msg = document.createElement('div'); | |
| msg.className = 'bg-blue-600 text-white p-3 rounded-2xl max-w-[80%] whitespace-pre-line shadow-sm'; | |
| msg.innerHTML = txt + `<div class='text-xs text-gray-200 mt-1 text-right'>${new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}</div>`; | |
| div.innerHTML = avatar; | |
| div.appendChild(msg); | |
| } | |
| messagesEl.appendChild(div); | |
| messagesEl.scrollTop = messagesEl.scrollHeight; | |
| } | |
| // Helper: Add typing indicator | |
| function addTyping(){ | |
| const div = document.createElement('div'); | |
| div.className = 'typing self-start flex space-x-1'; | |
| div.innerHTML = '<span></span><span></span><span></span>'; | |
| messagesEl.appendChild(div); | |
| messagesEl.scrollTop = messagesEl.scrollHeight; | |
| return div; | |
| } | |
| // Helper: Handle yes/no button clicks | |
| function handleYesNo(val) { | |
| addMessage(val,'user'); | |
| const q = queue[idx]; | |
| if(q && q.type==='yesno'){ | |
| state[q.key] = val.toLowerCase().startsWith('y'); | |
| } | |
| idx++; | |
| ask(); | |
| } | |
| // Main logic | |
| async function ask(){ | |
| if(awaitingCustom){ | |
| const q = queue[idx]; | |
| if(customIdx < customCount){ | |
| if(state.customStep==='name'){ | |
| addMessage(`Name of expense #${customIdx+1}?`,'bot'); | |
| renderInput({type:'text'}); | |
| return; | |
| } else { | |
| addMessage(`Amount for '${state.current}' ₹`,'bot'); | |
| renderInput({type:'number'}); | |
| return; | |
| } | |
| } else { | |
| awaitingCustom = false; | |
| idx++; | |
| } | |
| } | |
| if(idx < queue.length){ | |
| const q = queue[idx]; | |
| if(q.depends && !state[q.depends]){ idx++; return ask(); } | |
| addMessage(q.text,'bot'); | |
| renderInput(q); | |
| } else { | |
| // submit | |
| addMessage('Fetching suggestions…','bot'); | |
| const spinner = addTyping(); | |
| const payload = { | |
| salary: state.salary, | |
| risk_tolerance: 'moderate', | |
| expenses: state.expenses | |
| }; | |
| const res = await fetch('/analyze', { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify(payload) | |
| }); | |
| spinner.remove(); | |
| const json = await res.json(); | |
| addMessage(json.groq_advice_markdown,'bot'); | |
| inputArea.innerHTML = ''; | |
| sendBtn.disabled = true; | |
| } | |
| } | |
| // Send button handler | |
| sendBtn.onclick = async ()=>{ | |
| let el = inputArea.querySelector('input'); | |
| let val = el ? el.value.trim() : ''; | |
| if(!val) return; | |
| addMessage(val,'user'); | |
| const q = queue[idx]; | |
| // handle custom | |
| if(q && q.key==='custom_count'){ | |
| customCount = parseInt(val); | |
| state.customStep = 'name'; | |
| awaitingCustom = true; | |
| if(el) el.value=''; | |
| return ask(); | |
| } | |
| if(awaitingCustom){ | |
| if(state.customStep==='name'){ | |
| state.current = val; | |
| state.customStep = 'amount'; | |
| } else { | |
| if(!state.expenses.custom) state.expenses.custom = {}; | |
| state.expenses.custom[state.current] = parseFloat(val); | |
| customIdx++; | |
| state.customStep = 'name'; | |
| } | |
| if(el) el.value=''; | |
| return ask(); | |
| } | |
| // store normal | |
| if(q && q.type==='number'){ | |
| const num = parseFloat(val); | |
| if(q.key==='salary') state.salary = num; | |
| else { | |
| const cat = q.cat || 'custom'; | |
| state.expenses[cat][q.key] = num; | |
| // Show balance after each expense | |
| let spent = 0; | |
| Object.values(state.expenses).forEach(catObj => | |
| Object.values(catObj).forEach(amt => spent+=amt)); | |
| let balance = (state.salary||0) - spent; | |
| updateBalanceBar(balance, state.salary); | |
| addMessage(`Paid ${q.key.replace(/_/g,' ')} → ₹${num.toLocaleString()}, Balance: ₹${balance.toLocaleString()}`,'bot'); | |
| } | |
| } else if(q && q.key==='name'){ | |
| state.name = val; | |
| } | |
| idx++; | |
| if(el) el.value=''; | |
| ask(); | |
| }; | |
| // Initialize | |
| ask(); | |
| // Add the Marked.js CDN for markdown support | |
| if (!window.marked) { | |
| const script = document.createElement('script'); | |
| script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js'; | |
| document.head.appendChild(script); | |
| } | |