Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>π° FinFlow Pro</title> | |
| <style> | |
| :root { | |
| --bg: #0d1117; | |
| --bg2: #161b22; | |
| --card: #21262d; | |
| --border: #30363d; | |
| --accent: #58a6ff; | |
| --green: #3fb950; | |
| --red: #f85149; | |
| --yellow: #d29922; | |
| --text: #c9d1d9; | |
| --muted: #8b949e; | |
| --radius: 12px; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; min-height: 100vh; } | |
| .app { max-width: 960px; margin: 0 auto; padding: 20px; } | |
| header { display: flex; align-items: center; justify-content: space-between; padding: 20px 0 24px; border-bottom: 1px solid var(--border); margin-bottom: 24px; } | |
| .logo { display: flex; align-items: center; gap: 10px; } | |
| .logo-icon { font-size: 2rem; } | |
| .logo h1 { font-size: 1.4rem; color: var(--accent); } | |
| .logo p { font-size: 0.75rem; color: var(--muted); } | |
| .balance-badge { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 18px; text-align: right; } | |
| .balance-badge .label { font-size: 0.7rem; color: var(--muted); } | |
| .balance-badge .amount { font-size: 1.5rem; font-weight: bold; color: var(--green); } | |
| nav { display: flex; gap: 6px; margin-bottom: 24px; flex-wrap: wrap; } | |
| .nav-btn { background: none; border: 1px solid var(--border); color: var(--muted); padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 0.85rem; font-family: inherit; transition: all 0.15s; } | |
| .nav-btn.active, .nav-btn:hover { border-color: var(--accent); color: var(--accent); background: rgba(88,166,255,0.08); } | |
| .panel { display: none; } | |
| .panel.active { display: block; } | |
| .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } | |
| @media (max-width: 600px) { .grid-2 { grid-template-columns: 1fr; } } | |
| .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } | |
| @media (max-width: 700px) { .grid-3 { grid-template-columns: 1fr 1fr; } } | |
| .card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; } | |
| .card h3 { font-size: 0.85rem; color: var(--muted); margin-bottom: 8px; } | |
| .card .value { font-size: 1.8rem; font-weight: bold; } | |
| .card .change { font-size: 0.75rem; margin-top: 4px; } | |
| .up { color: var(--green); } | |
| .down { color: var(--red); } | |
| .neutral { color: var(--yellow); } | |
| .section-title { font-size: 1rem; font-weight: bold; margin-bottom: 14px; color: var(--text); } | |
| .full-card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 16px; } | |
| /* Budget */ | |
| .budget-row { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; } | |
| .budget-row .label { flex: 0 0 130px; font-size: 0.85rem; } | |
| .progress { flex: 1; background: var(--bg2); border-radius: 6px; height: 10px; overflow: hidden; } | |
| .progress-bar { height: 100%; border-radius: 6px; transition: width 0.5s; } | |
| .budget-row .pct { flex: 0 0 40px; font-size: 0.8rem; text-align: right; } | |
| /* Transactions */ | |
| .tx-row { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border); } | |
| .tx-row:last-child { border-bottom: none; } | |
| .tx-icon { font-size: 1.4rem; } | |
| .tx-info { flex: 1; } | |
| .tx-name { font-size: 0.9rem; } | |
| .tx-cat { font-size: 0.75rem; color: var(--muted); } | |
| .tx-amount { font-weight: bold; font-size: 0.95rem; } | |
| /* Forms */ | |
| .form-row { display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; } | |
| .form-row input, .form-row select { | |
| flex: 1; min-width: 120px; background: var(--bg2); border: 1px solid var(--border); color: var(--text); | |
| padding: 9px 12px; border-radius: 8px; font-size: 0.9rem; font-family: inherit; | |
| } | |
| .form-row input:focus, .form-row select:focus { outline: none; border-color: var(--accent); } | |
| select option { background: var(--bg2); } | |
| .btn { background: var(--accent); color: #0d1117; padding: 9px 18px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; font-size: 0.9rem; font-family: inherit; transition: opacity 0.15s; } | |
| .btn:hover { opacity: 0.85; } | |
| .btn.danger { background: var(--red); color: white; } | |
| .btn.ghost { background: none; border: 1px solid var(--border); color: var(--text); } | |
| .btn.ghost:hover { border-color: var(--accent); color: var(--accent); } | |
| /* Goals */ | |
| .goal-card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; margin-bottom: 12px; } | |
| .goal-top { display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px; } | |
| .goal-name { font-weight: bold; } | |
| .goal-pct { font-size: 0.75rem; color: var(--muted); } | |
| .goal-progress { background: var(--bg2); border-radius: 6px; height: 8px; overflow: hidden; margin-bottom: 8px; } | |
| .goal-bar { height: 100%; background: var(--accent); border-radius: 6px; transition: width 0.5s; } | |
| .goal-amounts { font-size: 0.8rem; color: var(--muted); } | |
| /* AI Chat */ | |
| .chat-box { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; height: 300px; overflow-y: auto; margin-bottom: 12px; } | |
| .msg { margin-bottom: 12px; } | |
| .msg.user { text-align: right; } | |
| .msg.user .bubble { background: var(--accent); color: #0d1117; display: inline-block; padding: 8px 14px; border-radius: 16px 16px 4px 16px; font-size: 0.9rem; max-width: 75%; } | |
| .msg.ai .bubble { background: var(--card); color: var(--text); display: inline-block; padding: 8px 14px; border-radius: 16px 16px 16px 4px; font-size: 0.9rem; max-width: 85%; white-space: pre-line; } | |
| .msg .name { font-size: 0.7rem; color: var(--muted); margin-bottom: 3px; } | |
| .chat-input-row { display: flex; gap: 8px; } | |
| .chat-input-row input { flex: 1; background: var(--bg2); border: 1px solid var(--border); color: var(--text); padding: 10px 14px; border-radius: 8px; font-family: inherit; font-size: 0.9rem; } | |
| .chat-input-row input:focus { outline: none; border-color: var(--accent); } | |
| .tag { display: inline-block; padding: 2px 8px; border-radius: 6px; font-size: 0.7rem; font-weight: bold; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <header> | |
| <div class="logo"> | |
| <div class="logo-icon">π°</div> | |
| <div> | |
| <h1>FinFlow Pro</h1> | |
| <p>Personal Finance Dashboard</p> | |
| </div> | |
| </div> | |
| <div class="balance-badge"> | |
| <div class="label">Net Worth</div> | |
| <div class="amount" id="netWorthDisplay">$0</div> | |
| </div> | |
| </header> | |
| <nav> | |
| <button class="nav-btn active" onclick="showPanel('dashboard')">π Dashboard</button> | |
| <button class="nav-btn" onclick="showPanel('transactions')">π³ Transactions</button> | |
| <button class="nav-btn" onclick="showPanel('budget')">π Budget</button> | |
| <button class="nav-btn" onclick="showPanel('goals')">π― Goals</button> | |
| <button class="nav-btn" onclick="showPanel('ai')">π€ AI Advisor</button> | |
| </nav> | |
| <!-- DASHBOARD --> | |
| <div id="panel-dashboard" class="panel active"> | |
| <div class="grid-3" style="margin-bottom:20px"> | |
| <div class="card"> | |
| <h3>Monthly Income</h3> | |
| <div class="value up" id="incomeDisplay">$0</div> | |
| <div class="change up">β This month</div> | |
| </div> | |
| <div class="card"> | |
| <h3>Monthly Expenses</h3> | |
| <div class="value down" id="expenseDisplay">$0</div> | |
| <div class="change down">β Spending</div> | |
| </div> | |
| <div class="card"> | |
| <h3>Monthly Savings</h3> | |
| <div class="value" id="savingsDisplay" style="color:var(--accent)">$0</div> | |
| <div class="change neutral">Income - Expenses</div> | |
| </div> | |
| </div> | |
| <div class="full-card"> | |
| <div class="section-title">π³ Recent Transactions</div> | |
| <div id="recentTxList"><p style="color:var(--muted);font-size:0.85rem">No transactions yet. Add some in the Transactions tab!</p></div> | |
| </div> | |
| <div class="full-card"> | |
| <div class="section-title">π Budget Overview</div> | |
| <div id="dashBudget"><p style="color:var(--muted);font-size:0.85rem">No budget set. Configure in the Budget tab!</p></div> | |
| </div> | |
| </div> | |
| <!-- TRANSACTIONS --> | |
| <div id="panel-transactions" class="panel"> | |
| <div class="full-card" style="margin-bottom:16px"> | |
| <div class="section-title">β Add Transaction</div> | |
| <div class="form-row"> | |
| <input type="text" id="txName" placeholder="Description (e.g. Grocery run)"> | |
| <input type="number" id="txAmount" placeholder="Amount (e.g. 85.50)" step="0.01"> | |
| <select id="txType"> | |
| <option value="expense">πΈ Expense</option> | |
| <option value="income">π΅ Income</option> | |
| </select> | |
| <select id="txCat"> | |
| <option value="Food">π Food</option> | |
| <option value="Transport">π Transport</option> | |
| <option value="Housing">π Housing</option> | |
| <option value="Entertainment">π¬ Entertainment</option> | |
| <option value="Health">π Health</option> | |
| <option value="Shopping">ποΈ Shopping</option> | |
| <option value="Salary">πΌ Salary</option> | |
| <option value="Other">π¦ Other</option> | |
| </select> | |
| <button class="btn" onclick="addTransaction()">Add</button> | |
| </div> | |
| </div> | |
| <div class="full-card"> | |
| <div class="section-title">π All Transactions</div> | |
| <div id="txList"><p style="color:var(--muted);font-size:0.85rem">No transactions yet.</p></div> | |
| </div> | |
| </div> | |
| <!-- BUDGET --> | |
| <div id="panel-budget" class="panel"> | |
| <div class="full-card" style="margin-bottom:16px"> | |
| <div class="section-title">βοΈ Set Monthly Budget</div> | |
| <div class="form-row"> | |
| <select id="budgetCat"> | |
| <option value="Food">π Food</option> | |
| <option value="Transport">π Transport</option> | |
| <option value="Housing">π Housing</option> | |
| <option value="Entertainment">π¬ Entertainment</option> | |
| <option value="Health">π Health</option> | |
| <option value="Shopping">ποΈ Shopping</option> | |
| <option value="Other">π¦ Other</option> | |
| </select> | |
| <input type="number" id="budgetAmount" placeholder="Monthly budget ($)" step="1"> | |
| <button class="btn" onclick="setBudget()">Set Budget</button> | |
| </div> | |
| </div> | |
| <div class="full-card"> | |
| <div class="section-title">π Budget vs. Actual</div> | |
| <div id="budgetList"><p style="color:var(--muted);font-size:0.85rem">Set a budget above to get started!</p></div> | |
| </div> | |
| </div> | |
| <!-- GOALS --> | |
| <div id="panel-goals" class="panel"> | |
| <div class="full-card" style="margin-bottom:16px"> | |
| <div class="section-title">π Add Savings Goal</div> | |
| <div class="form-row"> | |
| <input type="text" id="goalName" placeholder="Goal name (e.g. Emergency Fund)"> | |
| <input type="number" id="goalTarget" placeholder="Target amount ($)" step="1"> | |
| <input type="number" id="goalCurrent" placeholder="Current amount ($)" step="1"> | |
| <button class="btn" onclick="addGoal()">Add Goal</button> | |
| </div> | |
| </div> | |
| <div id="goalsList"><p style="color:var(--muted);font-size:0.85rem">No goals set yet.</p></div> | |
| </div> | |
| <!-- AI ADVISOR --> | |
| <div id="panel-ai" class="panel"> | |
| <div class="full-card"> | |
| <div class="section-title">π€ AI Financial Advisor</div> | |
| <p style="color:var(--muted);font-size:0.85rem;margin-bottom:12px">Powered by Grok xAI. Add your API key to unlock personalized advice. Get yours free at <a href="https://x.ai" target="_blank" style="color:var(--accent)">x.ai</a>.</p> | |
| <div class="form-row" style="margin-bottom:12px"> | |
| <input type="password" id="aiKey" placeholder="xai-... (optional)" oninput="saveAiKey(this.value)"> | |
| </div> | |
| <div class="chat-box" id="chatBox"></div> | |
| <div class="chat-input-row"> | |
| <input type="text" id="chatInput" placeholder="Ask about your finances..." onkeydown="if(event.key==='Enter')sendAiMsg()"> | |
| <button class="btn" onclick="sendAiMsg()">Send</button> | |
| </div> | |
| <div style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap"> | |
| <button class="btn ghost" onclick="quickQ('How should I allocate my income?')">π‘ Income allocation</button> | |
| <button class="btn ghost" onclick="quickQ('Tips to reduce my expenses')">βοΈ Cut expenses</button> | |
| <button class="btn ghost" onclick="quickQ('Best strategies to build an emergency fund')">π¨ Emergency fund</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const CAT_ICONS = { Food:'π', Transport:'π', Housing:'π ', Entertainment:'π¬', Health:'π', Shopping:'ποΈ', Salary:'πΌ', Other:'π¦' }; | |
| let data = { transactions: [], budgets: {}, goals: [] }; | |
| function save() { localStorage.setItem('ff-data', JSON.stringify(data)); } | |
| function load() { try { data = JSON.parse(localStorage.getItem('ff-data')) || { transactions: [], budgets: {}, goals: [] }; } catch { } } | |
| function saveAiKey(v) { if(v) localStorage.setItem('ff-ai-key', v); } | |
| function showPanel(name) { | |
| document.querySelectorAll('.nav-btn').forEach(b => b.classList.toggle('active', b.getAttribute('onclick').includes(name))); | |
| document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); | |
| document.getElementById('panel-' + name).classList.add('active'); | |
| if (name === 'dashboard') renderDashboard(); | |
| if (name === 'transactions') renderTransactions(); | |
| if (name === 'budget') renderBudget(); | |
| if (name === 'goals') renderGoals(); | |
| } | |
| function fmt(n) { return '$' + Math.abs(n).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}); } | |
| function addTransaction() { | |
| const name = document.getElementById('txName').value.trim(); | |
| const amount = parseFloat(document.getElementById('txAmount').value); | |
| const type = document.getElementById('txType').value; | |
| const cat = document.getElementById('txCat').value; | |
| if (!name || !amount) return alert('Please fill in description and amount.'); | |
| data.transactions.unshift({ id: Date.now(), name, amount, type, cat, date: new Date().toLocaleDateString() }); | |
| save(); | |
| document.getElementById('txName').value = ''; | |
| document.getElementById('txAmount').value = ''; | |
| renderTransactions(); | |
| renderDashboard(); | |
| } | |
| function renderTransactions() { | |
| const el = document.getElementById('txList'); | |
| if (!data.transactions.length) { el.innerHTML = '<p style="color:var(--muted);font-size:0.85rem">No transactions yet.</p>'; return; } | |
| el.innerHTML = data.transactions.map(t => ` | |
| <div class="tx-row"> | |
| <div class="tx-icon">${CAT_ICONS[t.cat]||'π¦'}</div> | |
| <div class="tx-info"><div class="tx-name">${t.name}</div><div class="tx-cat">${t.cat} Β· ${t.date}</div></div> | |
| <div class="tx-amount ${t.type==='income'?'up':'down'}">${t.type==='income'?'+':'-'}${fmt(t.amount)}</div> | |
| <button class="btn danger" style="padding:4px 10px;font-size:0.75rem" onclick="deleteTx(${t.id})">β</button> | |
| </div>`).join(''); | |
| } | |
| function deleteTx(id) { | |
| data.transactions = data.transactions.filter(t => t.id !== id); | |
| save(); renderTransactions(); renderDashboard(); | |
| } | |
| function getMonthlyTotals() { | |
| const now = new Date(); | |
| const month = now.getMonth(); const year = now.getFullYear(); | |
| const monthly = data.transactions.filter(t => { | |
| const d = new Date(t.date); return d.getMonth() === month && d.getFullYear() === year; | |
| }); | |
| const income = monthly.filter(t => t.type === 'income').reduce((s, t) => s + t.amount, 0); | |
| const expenses = monthly.filter(t => t.type === 'expense').reduce((s, t) => s + t.amount, 0); | |
| const byCategory = {}; | |
| monthly.filter(t => t.type === 'expense').forEach(t => { byCategory[t.cat] = (byCategory[t.cat]||0) + t.amount; }); | |
| return { income, expenses, savings: income - expenses, byCategory }; | |
| } | |
| function renderDashboard() { | |
| const { income, expenses, savings, byCategory } = getMonthlyTotals(); | |
| document.getElementById('incomeDisplay').textContent = fmt(income); | |
| document.getElementById('expenseDisplay').textContent = fmt(expenses); | |
| document.getElementById('savingsDisplay').textContent = fmt(savings); | |
| document.getElementById('savingsDisplay').className = 'value ' + (savings >= 0 ? 'up' : 'down'); | |
| const netWorth = data.goals.reduce((s, g) => s + g.current, 0) + (income - expenses) * 3; | |
| document.getElementById('netWorthDisplay').textContent = fmt(netWorth); | |
| const recent = data.transactions.slice(0, 5); | |
| document.getElementById('recentTxList').innerHTML = recent.length | |
| ? recent.map(t => `<div class="tx-row"><div class="tx-icon">${CAT_ICONS[t.cat]||'π¦'}</div><div class="tx-info"><div class="tx-name">${t.name}</div><div class="tx-cat">${t.cat}</div></div><div class="tx-amount ${t.type==='income'?'up':'down'}">${t.type==='income'?'+':'-'}${fmt(t.amount)}</div></div>`).join('') | |
| : '<p style="color:var(--muted);font-size:0.85rem">No recent transactions.</p>'; | |
| const budgetHTML = Object.entries(data.budgets).map(([cat, budget]) => { | |
| const spent = byCategory[cat] || 0; | |
| const pct = Math.min(100, Math.round((spent / budget) * 100)); | |
| const color = pct > 90 ? 'var(--red)' : pct > 70 ? 'var(--yellow)' : 'var(--green)'; | |
| return `<div class="budget-row"><div class="label">${CAT_ICONS[cat]||'π¦'} ${cat}</div><div class="progress"><div class="progress-bar" style="width:${pct}%;background:${color}"></div></div><div class="pct" style="color:${color}">${pct}%</div></div>`; | |
| }).join(''); | |
| document.getElementById('dashBudget').innerHTML = budgetHTML || '<p style="color:var(--muted);font-size:0.85rem">Set a budget to see overview.</p>'; | |
| } | |
| function setBudget() { | |
| const cat = document.getElementById('budgetCat').value; | |
| const amount = parseFloat(document.getElementById('budgetAmount').value); | |
| if (!amount) return alert('Enter a valid budget amount.'); | |
| data.budgets[cat] = amount; | |
| save(); | |
| document.getElementById('budgetAmount').value = ''; | |
| renderBudget(); | |
| } | |
| function renderBudget() { | |
| const el = document.getElementById('budgetList'); | |
| if (!Object.keys(data.budgets).length) { el.innerHTML = '<p style="color:var(--muted);font-size:0.85rem">Set a budget above to get started!</p>'; return; } | |
| const { byCategory } = getMonthlyTotals(); | |
| el.innerHTML = Object.entries(data.budgets).map(([cat, budget]) => { | |
| const spent = byCategory[cat] || 0; | |
| const pct = Math.min(100, Math.round((spent / budget) * 100)); | |
| const color = pct > 90 ? 'var(--red)' : pct > 70 ? 'var(--yellow)' : 'var(--green)'; | |
| return `<div class="budget-row" style="margin-bottom:16px"> | |
| <div class="label" style="flex:0 0 120px">${CAT_ICONS[cat]||'π¦'} ${cat}</div> | |
| <div class="progress"><div class="progress-bar" style="width:${pct}%;background:${color}"></div></div> | |
| <div class="pct" style="color:${color};flex:0 0 50px">${pct}%</div> | |
| <div style="font-size:0.75rem;color:var(--muted);flex:0 0 120px">${fmt(spent)} / ${fmt(budget)}</div> | |
| <button class="btn danger" style="padding:4px 8px;font-size:0.75rem" onclick="deleteBudget('${cat}')">β</button> | |
| </div>`; | |
| }).join(''); | |
| } | |
| function deleteBudget(cat) { delete data.budgets[cat]; save(); renderBudget(); renderDashboard(); } | |
| function addGoal() { | |
| const name = document.getElementById('goalName').value.trim(); | |
| const target = parseFloat(document.getElementById('goalTarget').value); | |
| const current = parseFloat(document.getElementById('goalCurrent').value) || 0; | |
| if (!name || !target) return alert('Please fill in goal name and target amount.'); | |
| data.goals.push({ id: Date.now(), name, target, current }); | |
| save(); | |
| document.getElementById('goalName').value = ''; | |
| document.getElementById('goalTarget').value = ''; | |
| document.getElementById('goalCurrent').value = ''; | |
| renderGoals(); | |
| } | |
| function renderGoals() { | |
| const el = document.getElementById('goalsList'); | |
| if (!data.goals.length) { el.innerHTML = '<p style="color:var(--muted);font-size:0.85rem">No goals set yet.</p>'; return; } | |
| el.innerHTML = data.goals.map(g => { | |
| const pct = Math.min(100, Math.round((g.current / g.target) * 100)); | |
| return `<div class="goal-card"> | |
| <div class="goal-top"><div class="goal-name">π― ${g.name}</div><div class="goal-pct">${pct}% complete</div></div> | |
| <div class="goal-progress"><div class="goal-bar" style="width:${pct}%"></div></div> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px"> | |
| <div class="goal-amounts">${fmt(g.current)} saved of ${fmt(g.target)} goal</div> | |
| <div style="display:flex;gap:8px"> | |
| <button class="btn ghost" style="padding:4px 10px;font-size:0.75rem" onclick="contributeGoal(${g.id})">+ Add</button> | |
| <button class="btn danger" style="padding:4px 10px;font-size:0.75rem" onclick="deleteGoal(${g.id})">β</button> | |
| </div> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| } | |
| function contributeGoal(id) { | |
| const amt = parseFloat(prompt('How much to add to this goal?')); | |
| if (!amt) return; | |
| const g = data.goals.find(g => g.id === id); | |
| if (g) { g.current = Math.min(g.target, g.current + amt); save(); renderGoals(); } | |
| } | |
| function deleteGoal(id) { data.goals = data.goals.filter(g => g.id !== id); save(); renderGoals(); } | |
| // AI Chat | |
| function quickQ(q) { document.getElementById('chatInput').value = q; sendAiMsg(); } | |
| async function sendAiMsg() { | |
| const input = document.getElementById('chatInput'); | |
| const msg = input.value.trim(); | |
| if (!msg) return; | |
| input.value = ''; | |
| const chatBox = document.getElementById('chatBox'); | |
| chatBox.innerHTML += `<div class="msg user"><div class="name">You</div><div class="bubble">${msg}</div></div>`; | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| const apiKey = document.getElementById('aiKey').value.trim() || localStorage.getItem('ff-ai-key') || ''; | |
| const { income, expenses, savings, byCategory } = getMonthlyTotals(); | |
| const context = `User's financial data: Monthly income: ${fmt(income)}, expenses: ${fmt(expenses)}, savings: ${fmt(savings)}. Expense breakdown: ${JSON.stringify(byCategory)}. Active goals: ${data.goals.map(g => `${g.name}: ${fmt(g.current)}/${fmt(g.target)}`).join(', ') || 'none'}.`; | |
| if (!apiKey) { | |
| const tips = [ | |
| "Based on your data, try the 50/30/20 rule: 50% needs, 30% wants, 20% savings.", | |
| "Building an emergency fund of 3-6 months of expenses is a great first goal!", | |
| "Track every expense for 30 days to find hidden spending patterns.", | |
| "Automate your savings by setting up automatic transfers on payday.", | |
| "Review subscriptions monthly β most people have 2-3 forgotten ones.", | |
| ]; | |
| const tip = tips[Math.floor(Math.random() * tips.length)]; | |
| chatBox.innerHTML += `<div class="msg ai"><div class="name">π° FinFlow AI</div><div class="bubble">π‘ ${tip}\n\n(Add your xAI API key above for personalized advice based on your actual data!)</div></div>`; | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| return; | |
| } | |
| chatBox.innerHTML += `<div class="msg ai" id="typing"><div class="name">π° FinFlow AI</div><div class="bubble">Thinking...</div></div>`; | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| try { | |
| const res = await fetch('https://api.x.ai/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, | |
| body: JSON.stringify({ | |
| model: 'grok-3-latest', | |
| messages: [ | |
| { role: 'system', content: 'You are a helpful personal finance advisor. Be concise, practical, and encouraging. ' + context }, | |
| { role: 'user', content: msg } | |
| ], | |
| max_tokens: 400 | |
| }) | |
| }); | |
| const d = await res.json(); | |
| const reply = d.choices?.[0]?.message?.content || 'Could not get a response.'; | |
| document.getElementById('typing')?.remove(); | |
| chatBox.innerHTML += `<div class="msg ai"><div class="name">π° FinFlow AI</div><div class="bubble">${reply}</div></div>`; | |
| } catch { | |
| document.getElementById('typing')?.remove(); | |
| chatBox.innerHTML += `<div class="msg ai"><div class="name">π° FinFlow AI</div><div class="bubble">π‘ Unable to connect to AI. Try the 50/30/20 rule: 50% needs, 30% wants, 20% savings!</div></div>`; | |
| } | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| } | |
| // Init | |
| load(); | |
| renderDashboard(); | |
| const savedKey = localStorage.getItem('ff-ai-key'); | |
| if (savedKey) document.getElementById('aiKey').value = savedKey; | |
| // Add welcome message | |
| document.getElementById('chatBox').innerHTML = '<div class="msg ai"><div class="name">π° FinFlow AI</div><div class="bubble">π Welcome to FinFlow Pro! Add your transactions, set a budget, and create savings goals to get started.\n\nAsk me anything about personal finance, or add your xAI API key above for personalized advice!</div></div>'; | |
| </script> | |
| </body> | |
| </html> | |