triflix commited on
Commit
9ea2a49
·
verified ·
1 Parent(s): ec17c7e

Upload 7 files

Browse files
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>