Spaces:
Sleeping
Sleeping
Upload 7 files
Browse files- Dockerfile +28 -0
- __pycache__/main.cpython-312.pyc +0 -0
- main.py +81 -0
- requirements.txt +5 -0
- static/css/style.css +32 -0
- static/js/chat.js +238 -0
- templates/index.html +37 -0
Dockerfile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use the official Python image as a base
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Set environment variables
|
| 5 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 6 |
+
PYTHONUNBUFFERED=1 \
|
| 7 |
+
PORT=7860
|
| 8 |
+
|
| 9 |
+
# Set work directory
|
| 10 |
+
WORKDIR /app
|
| 11 |
+
|
| 12 |
+
# Install system dependencies
|
| 13 |
+
RUN apt-get update && \
|
| 14 |
+
apt-get install -y --no-install-recommends build-essential && \
|
| 15 |
+
rm -rf /var/lib/apt/lists/*
|
| 16 |
+
|
| 17 |
+
# Copy requirements and install Python dependencies
|
| 18 |
+
COPY requirements.txt ./
|
| 19 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 20 |
+
|
| 21 |
+
# Copy app files
|
| 22 |
+
COPY . .
|
| 23 |
+
|
| 24 |
+
# Expose the port Hugging Face Spaces expects
|
| 25 |
+
EXPOSE 7860
|
| 26 |
+
|
| 27 |
+
# Start the FastAPI app using uvicorn on port 7860
|
| 28 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
__pycache__/main.cpython-312.pyc
ADDED
|
Binary file (4.13 kB). View file
|
|
|
main.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Request, HTTPException
|
| 2 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from fastapi.templating import Jinja2Templates
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
from typing import Dict
|
| 7 |
+
from groq import Groq
|
| 8 |
+
|
| 9 |
+
# ─── CONFIG ─────────────────────────────────────────────
|
| 10 |
+
GROQ_API_KEY = "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"
|
| 11 |
+
MODEL = "llama-3.3-70b-versatile"
|
| 12 |
+
# ────────────────────────────────────────────────────────
|
| 13 |
+
|
| 14 |
+
app = FastAPI()
|
| 15 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 16 |
+
templates = Jinja2Templates(directory="templates")
|
| 17 |
+
|
| 18 |
+
class BudgetInput(BaseModel):
|
| 19 |
+
salary: float
|
| 20 |
+
risk_tolerance: str
|
| 21 |
+
expenses: Dict[str, Dict[str, float]]
|
| 22 |
+
|
| 23 |
+
@app.get("/", response_class=HTMLResponse)
|
| 24 |
+
async def get_chat(request: Request):
|
| 25 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
| 26 |
+
|
| 27 |
+
@app.post("/analyze")
|
| 28 |
+
async def analyze(data: BudgetInput):
|
| 29 |
+
try:
|
| 30 |
+
total = sum(
|
| 31 |
+
amt
|
| 32 |
+
for category in data.expenses.values()
|
| 33 |
+
for amt in category.values()
|
| 34 |
+
)
|
| 35 |
+
remaining = data.salary - total
|
| 36 |
+
|
| 37 |
+
# build messages
|
| 38 |
+
system_msg = {
|
| 39 |
+
"role": "system",
|
| 40 |
+
"content": (
|
| 41 |
+
"You are a helpful financial advisor assistant "
|
| 42 |
+
"Based on a user's monthly salary, categorized expenses, and risk tolerance, and don't gave them desclaimer about anything "
|
| 43 |
+
"provide secure, actionable investment suggestions in a friendly tone , make sure give them concise on point and stats of it , list of where can you invest, is there any way ."
|
| 44 |
+
)
|
| 45 |
+
}
|
| 46 |
+
lines = []
|
| 47 |
+
for cat, vals in data.expenses.items():
|
| 48 |
+
for name, amt in vals.items():
|
| 49 |
+
lines.append(f"{name}: ₹{amt:.2f}")
|
| 50 |
+
user_msg = {
|
| 51 |
+
"role": "user",
|
| 52 |
+
"content": (
|
| 53 |
+
f"I have a monthly salary of ₹{data.salary:.2f}.\n"
|
| 54 |
+
f"My expenses:\n" + "\n".join(lines) + "\n"
|
| 55 |
+
f"Remaining balance: ₹{remaining:.2f}\n"
|
| 56 |
+
f"My risk tolerance is '{data.risk_tolerance}'.\n"
|
| 57 |
+
"Please suggest how I should invest this amount securely."
|
| 58 |
+
)
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
client = Groq(api_key=GROQ_API_KEY)
|
| 62 |
+
resp = client.chat.completions.create(
|
| 63 |
+
model=MODEL,
|
| 64 |
+
messages=[system_msg, user_msg],
|
| 65 |
+
temperature=0.5,
|
| 66 |
+
max_tokens=1024,
|
| 67 |
+
top_p=1.0,
|
| 68 |
+
)
|
| 69 |
+
advice_md = resp.choices[0].message.content
|
| 70 |
+
|
| 71 |
+
return JSONResponse({
|
| 72 |
+
"summary": {
|
| 73 |
+
"salary": data.salary,
|
| 74 |
+
"total_expenses": total,
|
| 75 |
+
"remaining_balance": remaining,
|
| 76 |
+
"expense_breakdown": data.expenses,
|
| 77 |
+
},
|
| 78 |
+
"groq_advice_markdown": advice_md
|
| 79 |
+
})
|
| 80 |
+
except Exception as e:
|
| 81 |
+
raise HTTPException(status_code=500, detail=str(e))
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
jinja2
|
| 4 |
+
python-multipart # for template rendering
|
| 5 |
+
groq
|
static/css/style.css
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Fade-in animation */
|
| 2 |
+
@keyframes fadeIn {
|
| 3 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 4 |
+
to { opacity: 1; transform: translateY(0); }
|
| 5 |
+
}
|
| 6 |
+
.message {
|
| 7 |
+
animation: fadeIn 0.3s ease-out;
|
| 8 |
+
}
|
| 9 |
+
/* Typing indicator */
|
| 10 |
+
.typing {
|
| 11 |
+
width: 24px;
|
| 12 |
+
height: 24px;
|
| 13 |
+
position: relative;
|
| 14 |
+
}
|
| 15 |
+
.typing span {
|
| 16 |
+
display: block;
|
| 17 |
+
width: 6px; height: 6px;
|
| 18 |
+
background: #4B5563;
|
| 19 |
+
border-radius: 50%;
|
| 20 |
+
position: absolute;
|
| 21 |
+
bottom: 0;
|
| 22 |
+
animation: bounce 0.6s infinite ease-in-out;
|
| 23 |
+
}
|
| 24 |
+
.typing span:nth-child(1) { left: 0; animation-delay: 0s; }
|
| 25 |
+
.typing span:nth-child(2) { left: 9px; animation-delay: 0.2s; }
|
| 26 |
+
.typing span:nth-child(3) { left: 18px; animation-delay: 0.4s; }
|
| 27 |
+
|
| 28 |
+
@keyframes bounce {
|
| 29 |
+
0%, 100% { transform: translateY(0); }
|
| 30 |
+
50% { transform: translateY(-6px); }
|
| 31 |
+
}
|
| 32 |
+
|
static/js/chat.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const messagesEl = document.getElementById('messages');
|
| 2 |
+
const inputArea = document.getElementById('input-area');
|
| 3 |
+
const balanceBar = document.getElementById('balance-bar');
|
| 4 |
+
const balanceValue = document.getElementById('balance-value');
|
| 5 |
+
const balanceProgress = document.getElementById('balance-progress');
|
| 6 |
+
const sendBtn = document.getElementById('send');
|
| 7 |
+
|
| 8 |
+
let state = { expenses: { fixed: {}, optional: {}, custom: {} } };
|
| 9 |
+
let queue = [
|
| 10 |
+
{ key:'name', text:'Hello there! What’s your name?', type:'text' },
|
| 11 |
+
{ 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' },
|
| 12 |
+
{ key:'salary', text:'Enter your total monthly in-hand salary: ₹', type:'number' },
|
| 13 |
+
...[
|
| 14 |
+
'Rent/Mortgage','Groceries','Utilities','EMI/Loan'
|
| 15 |
+
].map(k=>({ key:k, text:`Enter your monthly ${k}: ₹`, type:'number', cat:'fixed' })),
|
| 16 |
+
...[
|
| 17 |
+
'Insurance','Transport','Gym','Subs'
|
| 18 |
+
].flatMap(k=>[
|
| 19 |
+
{ key:`has_${k}`, text:`Do you have ${k}? (y/n)`, type:'yesno' },
|
| 20 |
+
{ key:k, text:`Enter your monthly ${k}: ₹`, type:'number', depends:`has_${k}`, cat:'optional' }
|
| 21 |
+
]),
|
| 22 |
+
{ key:'has_custom', text:'Any other recurring expenses to add? (y/n)', type:'yesno' },
|
| 23 |
+
{ key:'custom_count', text:'How many?', type:'number', depends:'has_custom' },
|
| 24 |
+
// custom entries dynamic
|
| 25 |
+
];
|
| 26 |
+
|
| 27 |
+
let idx = 0, customCount = 0, customIdx = 0, awaitingCustom = false;
|
| 28 |
+
|
| 29 |
+
// Helper: Render input control based on question type
|
| 30 |
+
function renderInput(q) {
|
| 31 |
+
inputArea.innerHTML = '';
|
| 32 |
+
let el;
|
| 33 |
+
if(q.type==='yesno') {
|
| 34 |
+
el = document.createElement('div');
|
| 35 |
+
['Yes','No'].forEach(opt => {
|
| 36 |
+
const btn = document.createElement('button');
|
| 37 |
+
btn.textContent = opt;
|
| 38 |
+
btn.className = 'px-4 py-2 rounded-lg border bg-gray-50 hover:bg-blue-100 text-blue-700 font-semibold mx-1';
|
| 39 |
+
btn.onclick = () => {
|
| 40 |
+
inputArea.querySelectorAll('button').forEach(b=>b.disabled=true);
|
| 41 |
+
handleYesNo(opt);
|
| 42 |
+
};
|
| 43 |
+
el.appendChild(btn);
|
| 44 |
+
});
|
| 45 |
+
} else if(q.type==='number') {
|
| 46 |
+
el = document.createElement('input');
|
| 47 |
+
el.type = 'number';
|
| 48 |
+
el.className = 'flex-1 p-3 rounded-lg border bg-gray-100 focus:outline-none';
|
| 49 |
+
el.placeholder = 'Enter amount';
|
| 50 |
+
el.id = 'input';
|
| 51 |
+
el.oninput = () => sendBtn.disabled = !el.value;
|
| 52 |
+
} else {
|
| 53 |
+
el = document.createElement('input');
|
| 54 |
+
el.type = 'text';
|
| 55 |
+
el.className = 'flex-1 p-3 rounded-lg border bg-gray-100 focus:outline-none';
|
| 56 |
+
el.placeholder = 'Type your answer...';
|
| 57 |
+
el.id = 'input';
|
| 58 |
+
el.oninput = () => sendBtn.disabled = !el.value;
|
| 59 |
+
}
|
| 60 |
+
inputArea.appendChild(el);
|
| 61 |
+
if(q.type!=='yesno') {
|
| 62 |
+
sendBtn.disabled = true;
|
| 63 |
+
el.addEventListener('keydown', e => { if(e.key==='Enter' && el.value) sendBtn.click(); });
|
| 64 |
+
el.focus();
|
| 65 |
+
} else {
|
| 66 |
+
sendBtn.disabled = true;
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Helper: Update balance bar
|
| 71 |
+
function updateBalanceBar(balance, salary) {
|
| 72 |
+
if(salary && salary > 0) {
|
| 73 |
+
balanceBar.classList.remove('hidden');
|
| 74 |
+
balanceValue.textContent = `₹${balance.toLocaleString()}`;
|
| 75 |
+
let percent = Math.max(0, Math.min(100, Math.round((balance/salary)*100)));
|
| 76 |
+
balanceProgress.style.width = percent + '%';
|
| 77 |
+
balanceProgress.className =
|
| 78 |
+
'h-2 rounded-full ' + (percent > 60 ? 'bg-green-400' : percent > 30 ? 'bg-yellow-400' : 'bg-red-400');
|
| 79 |
+
} else {
|
| 80 |
+
balanceBar.classList.add('hidden');
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Helper: Add message to chat (supports markdown for bot)
|
| 85 |
+
function addMessage(txt, who){
|
| 86 |
+
const div = document.createElement('div');
|
| 87 |
+
div.className = 'message flex items-end gap-2 ' + (who==='bot'
|
| 88 |
+
? 'justify-start' : 'justify-end flex-row-reverse');
|
| 89 |
+
if(who==='bot') {
|
| 90 |
+
// Render markdown for bot replies
|
| 91 |
+
const avatar = `<img src="https://img.icons8.com/color/48/000000/bot.png" class="w-8 h-8 rounded-full border shadow bg-white">`;
|
| 92 |
+
const msg = document.createElement('div');
|
| 93 |
+
msg.className = 'bg-gray-100 text-gray-800 p-3 rounded-2xl max-w-[80%] whitespace-pre-line shadow-sm';
|
| 94 |
+
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>`;
|
| 95 |
+
div.innerHTML = avatar;
|
| 96 |
+
div.appendChild(msg);
|
| 97 |
+
} else {
|
| 98 |
+
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">`;
|
| 99 |
+
const msg = document.createElement('div');
|
| 100 |
+
msg.className = 'bg-blue-600 text-white p-3 rounded-2xl max-w-[80%] whitespace-pre-line shadow-sm';
|
| 101 |
+
msg.innerHTML = txt + `<div class='text-xs text-gray-200 mt-1 text-right'>${new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}</div>`;
|
| 102 |
+
div.innerHTML = avatar;
|
| 103 |
+
div.appendChild(msg);
|
| 104 |
+
}
|
| 105 |
+
messagesEl.appendChild(div);
|
| 106 |
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Helper: Add typing indicator
|
| 110 |
+
function addTyping(){
|
| 111 |
+
const div = document.createElement('div');
|
| 112 |
+
div.className = 'typing self-start flex space-x-1';
|
| 113 |
+
div.innerHTML = '<span></span><span></span><span></span>';
|
| 114 |
+
messagesEl.appendChild(div);
|
| 115 |
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
| 116 |
+
return div;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Helper: Handle yes/no button clicks
|
| 120 |
+
function handleYesNo(val) {
|
| 121 |
+
addMessage(val,'user');
|
| 122 |
+
const q = queue[idx];
|
| 123 |
+
if(q && q.type==='yesno'){
|
| 124 |
+
state[q.key] = val.toLowerCase().startsWith('y');
|
| 125 |
+
}
|
| 126 |
+
idx++;
|
| 127 |
+
ask();
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// Main logic
|
| 131 |
+
async function ask(){
|
| 132 |
+
if(awaitingCustom){
|
| 133 |
+
const q = queue[idx];
|
| 134 |
+
if(customIdx < customCount){
|
| 135 |
+
if(state.customStep==='name'){
|
| 136 |
+
addMessage(`Name of expense #${customIdx+1}?`,'bot');
|
| 137 |
+
renderInput({type:'text'});
|
| 138 |
+
return;
|
| 139 |
+
} else {
|
| 140 |
+
addMessage(`Amount for '${state.current}' ₹`,'bot');
|
| 141 |
+
renderInput({type:'number'});
|
| 142 |
+
return;
|
| 143 |
+
}
|
| 144 |
+
} else {
|
| 145 |
+
awaitingCustom = false;
|
| 146 |
+
idx++;
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
if(idx < queue.length){
|
| 150 |
+
const q = queue[idx];
|
| 151 |
+
if(q.depends && !state[q.depends]){ idx++; return ask(); }
|
| 152 |
+
addMessage(q.text,'bot');
|
| 153 |
+
renderInput(q);
|
| 154 |
+
} else {
|
| 155 |
+
// submit
|
| 156 |
+
addMessage('Fetching suggestions…','bot');
|
| 157 |
+
const spinner = addTyping();
|
| 158 |
+
const payload = {
|
| 159 |
+
salary: state.salary,
|
| 160 |
+
risk_tolerance: 'moderate',
|
| 161 |
+
expenses: state.expenses
|
| 162 |
+
};
|
| 163 |
+
const res = await fetch('/analyze', {
|
| 164 |
+
method:'POST',
|
| 165 |
+
headers:{'Content-Type':'application/json'},
|
| 166 |
+
body: JSON.stringify(payload)
|
| 167 |
+
});
|
| 168 |
+
spinner.remove();
|
| 169 |
+
const json = await res.json();
|
| 170 |
+
addMessage(json.groq_advice_markdown,'bot');
|
| 171 |
+
inputArea.innerHTML = '';
|
| 172 |
+
sendBtn.disabled = true;
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// Send button handler
|
| 177 |
+
sendBtn.onclick = async ()=>{
|
| 178 |
+
let el = inputArea.querySelector('input');
|
| 179 |
+
let val = el ? el.value.trim() : '';
|
| 180 |
+
if(!val) return;
|
| 181 |
+
addMessage(val,'user');
|
| 182 |
+
const q = queue[idx];
|
| 183 |
+
|
| 184 |
+
// handle custom
|
| 185 |
+
if(q && q.key==='custom_count'){
|
| 186 |
+
customCount = parseInt(val);
|
| 187 |
+
state.customStep = 'name';
|
| 188 |
+
awaitingCustom = true;
|
| 189 |
+
if(el) el.value='';
|
| 190 |
+
return ask();
|
| 191 |
+
}
|
| 192 |
+
if(awaitingCustom){
|
| 193 |
+
if(state.customStep==='name'){
|
| 194 |
+
state.current = val;
|
| 195 |
+
state.customStep = 'amount';
|
| 196 |
+
} else {
|
| 197 |
+
if(!state.expenses.custom) state.expenses.custom = {};
|
| 198 |
+
state.expenses.custom[state.current] = parseFloat(val);
|
| 199 |
+
customIdx++;
|
| 200 |
+
state.customStep = 'name';
|
| 201 |
+
}
|
| 202 |
+
if(el) el.value='';
|
| 203 |
+
return ask();
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
// store normal
|
| 207 |
+
if(q && q.type==='number'){
|
| 208 |
+
const num = parseFloat(val);
|
| 209 |
+
if(q.key==='salary') state.salary = num;
|
| 210 |
+
else {
|
| 211 |
+
const cat = q.cat || 'custom';
|
| 212 |
+
state.expenses[cat][q.key] = num;
|
| 213 |
+
// Show balance after each expense
|
| 214 |
+
let spent = 0;
|
| 215 |
+
Object.values(state.expenses).forEach(catObj =>
|
| 216 |
+
Object.values(catObj).forEach(amt => spent+=amt));
|
| 217 |
+
let balance = (state.salary||0) - spent;
|
| 218 |
+
updateBalanceBar(balance, state.salary);
|
| 219 |
+
addMessage(`Paid ${q.key.replace(/_/g,' ')} → ₹${num.toLocaleString()}, Balance: ₹${balance.toLocaleString()}`,'bot');
|
| 220 |
+
}
|
| 221 |
+
} else if(q && q.key==='name'){
|
| 222 |
+
state.name = val;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
idx++;
|
| 226 |
+
if(el) el.value='';
|
| 227 |
+
ask();
|
| 228 |
+
};
|
| 229 |
+
|
| 230 |
+
// Initialize
|
| 231 |
+
ask();
|
| 232 |
+
|
| 233 |
+
// Add the Marked.js CDN for markdown support
|
| 234 |
+
if (!window.marked) {
|
| 235 |
+
const script = document.createElement('script');
|
| 236 |
+
script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
|
| 237 |
+
document.head.appendChild(script);
|
| 238 |
+
}
|
templates/index.html
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>AI Budget Advisor</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<link rel="stylesheet" href="/static/css/style.css" />
|
| 9 |
+
</head>
|
| 10 |
+
<body class="bg-gradient-to-br from-blue-50 via-white to-green-50 flex items-center justify-center min-h-screen font-sans">
|
| 11 |
+
<div class="w-full max-w-md bg-white rounded-3xl shadow-2xl overflow-hidden flex flex-col border border-blue-100">
|
| 12 |
+
<div class="flex items-center gap-2 px-6 py-4 bg-gradient-to-r from-blue-600 to-green-500">
|
| 13 |
+
<img src="https://img.icons8.com/color/48/000000/bot.png" alt="Bot" class="w-8 h-8 rounded-full bg-white p-1 shadow">
|
| 14 |
+
<h1 class="text-white text-lg font-bold tracking-wide">AI Finance Advisor</h1>
|
| 15 |
+
</div>
|
| 16 |
+
<div class="flex-1 px-4 py-6 space-y-4 overflow-y-auto bg-gradient-to-br from-white to-blue-50" id="messages" style="min-height:320px;"></div>
|
| 17 |
+
<footer class="w-full bg-white border-t pt-2 pb-2 flex flex-col gap-1 sticky bottom-0 z-10 shadow-xl">
|
| 18 |
+
<div class="flex items-center px-4">
|
| 19 |
+
<div id="input-area" class="flex-1 flex gap-2"></div>
|
| 20 |
+
<button id="send" class="p-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50" disabled>Send</button>
|
| 21 |
+
</div>
|
| 22 |
+
<div id="balance-bar" class="px-6 pb-1 pt-1 hidden">
|
| 23 |
+
<div class="flex justify-between text-xs text-gray-500 mb-1">
|
| 24 |
+
<span>Balance</span>
|
| 25 |
+
<span id="balance-value">₹0</span>
|
| 26 |
+
</div>
|
| 27 |
+
<div class="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
| 28 |
+
<div id="balance-progress" class="h-2 bg-green-400" style="width:100%"></div>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
</footer>
|
| 32 |
+
</div>
|
| 33 |
+
<!-- Marked.js for markdown rendering -->
|
| 34 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 35 |
+
<script src="/static/js/chat.js"></script>
|
| 36 |
+
</body>
|
| 37 |
+
</html>
|