triflix's picture
Upload 7 files
9ea2a49 verified
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);
}