umar8902 commited on
Commit
f06a80c
·
verified ·
1 Parent(s): a2d8b2c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +371 -491
app.py CHANGED
@@ -1,539 +1,419 @@
1
- import os, re, json, uuid, time, logging
2
- from datetime import datetime
3
-
4
- import requests
5
- from flask import Flask, request, jsonify, Response, send_from_directory
6
  from flask_cors import CORS
7
- from dotenv import load_dotenv
8
- import sqlite3
9
-
10
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  logger = logging.getLogger(__name__)
12
- load_dotenv()
13
-
14
- app = Flask(__name__, static_folder='static')
15
- CORS(app, supports_credentials=True)
16
- app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'ai-chat-secret-key-2026')
17
-
18
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
19
- UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
20
- DATA_FOLDER = os.path.join(BASE_DIR, 'data')
21
- AVATAR_FOLDER = os.path.join(DATA_FOLDER, 'avatars')
22
- DB_PATH = os.path.join(DATA_FOLDER, 'chat.db')
23
- ALLOWED_EXTENSIONS = {'txt','pdf','png','jpg','jpeg','gif','py','js','html','css','json','md','doc','docx','csv','xml','yaml','yml','sql','sh','bat'}
24
- MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB
25
 
26
- for d in [UPLOAD_FOLDER, DATA_FOLDER, AVATAR_FOLDER]:
27
- os.makedirs(d, exist_ok=True)
28
-
29
- def get_db():
30
- conn = sqlite3.connect(DB_PATH)
 
 
 
 
 
 
31
  conn.row_factory = sqlite3.Row
32
- conn.execute("PRAGMA journal_mode=WAL")
33
  return conn
34
 
35
  def init_db():
36
- with get_db() as conn:
37
- conn.executescript('''
38
- CREATE TABLE IF NOT EXISTS profile (
39
- id INTEGER PRIMARY KEY CHECK(id=1),
40
- display_name TEXT DEFAULT 'Guest',
41
- bio TEXT DEFAULT '',
42
- accent_color TEXT DEFAULT '#10b981',
43
- font_size TEXT DEFAULT 'medium',
44
- density TEXT DEFAULT 'comfortable',
45
- api_key TEXT DEFAULT '',
46
- model TEXT DEFAULT 'default',
47
- temperature REAL DEFAULT 0.7,
48
- avatar TEXT DEFAULT NULL,
49
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
50
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
51
- );
52
- INSERT OR IGNORE INTO profile (id, display_name) VALUES (1, 'Guest');
53
-
54
- CREATE TABLE IF NOT EXISTS conversations (
55
- id INTEGER PRIMARY KEY AUTOINCREMENT,
56
- title TEXT NOT NULL DEFAULT 'New Chat',
57
- model TEXT DEFAULT 'default',
58
- system_prompt TEXT DEFAULT '',
59
- persona TEXT DEFAULT 'default',
60
- pinned INTEGER DEFAULT 0,
61
- message_count INTEGER DEFAULT 0,
62
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
63
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
64
- );
65
-
66
- CREATE TABLE IF NOT EXISTS messages (
67
- id INTEGER PRIMARY KEY AUTOINCREMENT,
68
- conversation_id INTEGER NOT NULL,
69
- role TEXT NOT NULL CHECK(role IN ('user','assistant','system')),
70
- content TEXT NOT NULL,
71
- parent_id INTEGER DEFAULT NULL,
72
- feedback INTEGER DEFAULT NULL,
73
- feedback_text TEXT DEFAULT '',
74
- timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
75
- FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
76
- );
77
-
78
- CREATE TABLE IF NOT EXISTS files (
79
- id INTEGER PRIMARY KEY AUTOINCREMENT,
80
- filename TEXT NOT NULL,
81
- original_name TEXT NOT NULL,
82
- file_type TEXT NOT NULL,
83
- file_size INTEGER DEFAULT 0,
84
- conversation_id INTEGER DEFAULT NULL,
85
- uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
86
- );
87
-
88
- CREATE TABLE IF NOT EXISTS feedback (
89
- id INTEGER PRIMARY KEY AUTOINCREMENT,
90
- message_id INTEGER NOT NULL,
91
- rating INTEGER NOT NULL CHECK(rating IN (1,-1)),
92
- comment TEXT DEFAULT '',
93
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
94
- FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
95
- );
96
- ''')
97
- conn.commit()
98
 
99
  init_db()
100
 
101
- # ── Helpers ──────────────────────────────────────────────────────────────
102
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  def allowed_file(filename):
104
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
105
 
106
- def generate_title(text):
107
- text = re.sub(r'<[^>]+>', '', text).strip()
108
- return (text[:40] + '...') if len(text) > 40 else text or 'New Chat'
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
- def json_error(msg, code=400):
111
- return jsonify({'error': msg}), code
 
 
112
 
113
- # ── Profile Routes ───────────────────────────────────────────────────────
 
 
114
 
115
- @app.route('/api/profile', methods=['GET', 'POST'])
116
- def handle_profile():
117
- if request.method == 'GET':
118
- with get_db() as conn:
119
- p = conn.execute('SELECT * FROM profile WHERE id=1').fetchone()
120
- return jsonify(dict(p) if p else {'display_name': 'Guest'})
121
-
122
- data = request.json or {}
123
- allowed_keys = {'display_name', 'bio', 'accent_color', 'font_size', 'density', 'api_key', 'model', 'temperature', 'avatar'}
124
- updates = {k: v for k, v in data.items() if k in allowed_keys}
125
- if not updates:
126
- return json_error('No valid fields to update')
127
-
128
- set_clause = ', '.join(f'{k}=?' for k in updates)
129
- values = list(updates.values())
130
- values.append(datetime.now().isoformat())
131
 
132
- with get_db() as conn:
133
- conn.execute(f'UPDATE profile SET {set_clause}, updated_at=? WHERE id=1', values)
134
- conn.commit()
135
- p = conn.execute('SELECT * FROM profile WHERE id=1').fetchone()
136
- return jsonify(dict(p))
137
-
138
- @app.route('/api/profile/avatar', methods=['POST'])
139
- def upload_avatar():
140
- if 'avatar' not in request.files:
141
- return json_error('No avatar file provided')
142
- f = request.files['avatar']
143
- if not f.filename:
144
- return json_error('Empty filename')
145
 
146
- ext = f.filename.rsplit('.', 1)[1].lower() if '.' in f.filename else 'png'
147
- if ext not in {'png', 'jpg', 'jpeg', 'gif', 'webp'}:
148
- return json_error('Invalid image format')
 
 
 
 
 
 
 
149
 
150
- filename = f'avatar_{uuid.uuid4().hex}.{ext}'
151
- path = os.path.join(AVATAR_FOLDER, filename)
152
- f.save(path)
 
 
 
 
 
 
 
 
 
153
 
154
- with get_db() as conn:
155
- conn.execute('UPDATE profile SET avatar=? WHERE id=1', (filename,))
156
- conn.commit()
157
- return jsonify({'success': True, 'filename': filename, 'url': f'/api/profile/avatar/{filename}'})
158
-
159
- @app.route('/api/profile/avatar/<filename>')
160
- def serve_avatar(filename):
161
- return send_from_directory(AVATAR_FOLDER, filename)
162
-
163
- # ── File Upload Routes ───────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
  @app.route('/upload', methods=['POST'])
166
  def upload_file():
167
  if 'file' not in request.files:
168
- return json_error('No file provided')
169
- f = request.files['file']
170
- if not f.filename or not allowed_file(f.filename):
171
- return json_error('Invalid file type')
172
-
173
- f.seek(0, os.SEEK_END)
174
- size = f.tell()
175
- f.seek(0)
176
- if size > MAX_UPLOAD_SIZE:
177
- return json_error('File too large (max 10MB)')
178
-
179
- ext = f.filename.rsplit('.', 1)[1].lower()
180
- unique_name = f'{uuid.uuid4().hex}.{ext}'
181
- path = os.path.join(UPLOAD_FOLDER, unique_name)
182
- f.save(path)
183
-
184
- original_name = f.filename
185
- file_type = ext
186
-
187
- conv_id = request.form.get('conversation_id')
188
- with get_db() as conn:
189
- conn.execute(
190
- 'INSERT INTO files (filename, original_name, file_type, file_size, conversation_id) VALUES (?,?,?,?,?)',
191
- (unique_name, original_name, file_type, size, conv_id)
192
- )
193
- conn.commit()
194
-
195
- return jsonify({
196
- 'success': True,
197
- 'filename': unique_name,
198
- 'original_name': original_name,
199
- 'type': file_type,
200
- 'size': size,
201
- 'url': f'/uploads/{unique_name}'
202
- })
203
 
204
  @app.route('/uploads/<filename>')
205
  def serve_upload(filename):
206
  return send_from_directory(UPLOAD_FOLDER, filename)
207
 
208
- # ── Chat Routes ──────────────────────────────────────────────────────────
209
-
210
- SYSTEM_PROMPT = """You are an AI assistant. Provide helpful, accurate, and well-structured responses.
211
- Use markdown formatting: headings, lists, code blocks, tables, and math when appropriate.
212
- Be concise but thorough. Current date: {date}"""
213
-
214
- PERSONAS = {
215
- 'default': 'You are a helpful, general-purpose AI assistant.',
216
- 'creative': 'You are a creative AI. Use vivid language, metaphors, and imaginative examples. Be expressive and original.',
217
- 'precise': 'You are a precise AI. Give concise, factual answers. Use bullet points. Avoid speculation. Be direct.',
218
- 'coding': 'You are an expert programming AI. Provide clean, well-documented code. Explain your reasoning. Use code blocks with language tags.',
219
- }
220
 
221
- @app.route('/chat', methods=['POST'])
222
- def chat():
223
- data = request.json or {}
224
- message = data.get('message', '').strip()
225
- history = data.get('history', [])
226
- conv_id = data.get('conversation_id')
227
- persona = data.get('persona', 'default')
228
- temperature = float(data.get('temperature', 0.7))
229
-
230
- if not message:
231
- return json_error('No message provided')
232
-
233
- date_str = datetime.now().strftime('%Y-%m-%d')
234
- system = PERSONAS.get(persona, PERSONAS['default'])
235
- sys_prompt = f"{system}\n\n{SYSTEM_PROMPT.format(date=date_str)}"
236
-
237
- full_history = [{'role': 'system', 'content': sys_prompt}]
238
- for h in history:
239
- full_history.append({'role': h.get('role', 'user'), 'content': h.get('content', '')})
240
- full_history.append({'role': 'user', 'content': message})
241
-
242
- # Save user message
243
- if conv_id:
244
- with get_db() as conn:
245
- conn.execute('INSERT INTO messages (conversation_id, role, content) VALUES (?,?,?)',
246
- (conv_id, 'user', message))
247
- conn.execute('UPDATE conversations SET message_count=message_count+1, updated_at=CURRENT_TIMESTAMP WHERE id=?', (conv_id,))
248
- conn.commit()
249
-
250
- # Simulated response (in production, call an LLM API)
251
- response_text = _generate_response(message, full_history, temperature)
252
-
253
- # Save assistant message
254
- msg_id = None
255
- if conv_id:
256
- with get_db() as conn:
257
- cur = conn.execute('INSERT INTO messages (conversation_id, role, content) VALUES (?,?,?)',
258
- (conv_id, 'assistant', response_text))
259
- conn.execute('UPDATE conversations SET message_count=message_count+1, updated_at=CURRENT_TIMESTAMP WHERE id=?', (conv_id,))
260
- conn.commit()
261
- msg_id = cur.lastrowid
262
 
263
- return jsonify({
264
- 'response': response_text,
265
- 'message_id': msg_id,
266
- 'conversation_id': conv_id
267
- })
 
268
 
269
  @app.route('/chat/stream', methods=['POST'])
270
- def chat_stream():
271
- data = request.json or {}
272
- message = data.get('message', '').strip()
273
- history = data.get('history', [])
274
  conv_id = data.get('conversation_id')
275
- persona = data.get('persona', 'default')
276
- temperature = float(data.get('temperature', 0.7))
277
-
278
- if not message:
279
- return json_error('No message provided')
280
-
281
- date_str = datetime.now().strftime('%Y-%m-%d')
282
- system = PERSONAS.get(persona, PERSONAS['default'])
283
- sys_prompt = f"{system}\n\n{SYSTEM_PROMPT.format(date=date_str)}"
284
-
285
- if conv_id:
286
- with get_db() as conn:
287
- conn.execute('INSERT INTO messages (conversation_id, role, content) VALUES (?,?,?)',
288
- (conv_id, 'user', message))
289
- conn.execute('UPDATE conversations SET message_count=message_count+1, updated_at=CURRENT_TIMESTAMP WHERE id=?', (conv_id,))
290
- conn.commit()
291
 
 
 
 
 
 
292
  def generate():
293
- full_response = ''
294
- response_text = _generate_response(message,
295
- [{'role': 'system', 'content': sys_prompt}] + history + [{'role': 'user', 'content': message}],
296
- temperature)
 
 
 
 
 
 
297
 
298
- words = response_text.split(' ')
299
- for i, word in enumerate(words):
300
- chunk = word + (' ' if i < len(words) - 1 else '')
301
- full_response += chunk
302
- yield f"data: {json.dumps({'token': chunk, 'done': False})}\n\n"
303
- time.sleep(0.02)
304
-
305
- msg_id = None
306
- if conv_id:
307
- with get_db() as conn:
308
- cur = conn.execute('INSERT INTO messages (conversation_id, role, content) VALUES (?,?,?)',
309
- (conv_id, 'assistant', full_response))
310
- conn.execute('UPDATE conversations SET message_count=message_count+1, updated_at=CURRENT_TIMESTAMP WHERE id=?', (conv_id,))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  conn.commit()
312
- msg_id = cur.lastrowid
313
-
314
- yield f"data: {json.dumps({'token': '', 'done': True, 'message_id': msg_id, 'conversation_id': conv_id})}\n\n"
315
-
316
- return Response(generate(), mimetype='text/event-stream', headers={
317
- 'Cache-Control': 'no-cache',
318
- 'X-Accel-Buffering': 'no',
319
- })
320
 
321
- def _generate_response(message, messages, temperature=0.7):
322
- try:
323
- openai_key = os.environ.get('OPENAI_API_KEY')
324
- if openai_key:
325
- from openai import OpenAI
326
- client = OpenAI(api_key=openai_key)
327
- resp = client.chat.completions.create(
328
- model='gpt-4o-mini', messages=messages, temperature=temperature, max_tokens=2048
329
- )
330
- return resp.choices[0].message.content
331
- except Exception as e:
332
- logger.warning(f'OpenAI failed: {e}')
333
 
334
- try:
335
- ollama_url = os.environ.get('OLLAMA_URL', 'http://localhost:11434')
336
- r = requests.post(f'{ollama_url}/api/chat', json={
337
- 'model': 'llama3.2', 'messages': messages, 'stream': False,
338
- 'options': {'temperature': temperature, 'num_predict': 2048}
339
- }, timeout=30)
340
- if r.status_code == 200:
341
- return r.json()['message']['content']
342
- except Exception as e:
343
- logger.warning(f'Ollama failed: {e}')
344
 
345
- # Fallback response
346
- return _fallback_response(message)
347
-
348
- def _fallback_response(message):
349
- msg_lower = message.lower()
350
- if 'hello' in msg_lower or 'hi ' in msg_lower:
351
- return "Hello! How can I help you today?"
352
- if 'code' in msg_lower or 'function' in msg_lower or 'program' in msg_lower:
353
- return "Here's an example:\n\n```python\ndef greet(name):\n return f'Hello, {name}!'\n\nprint(greet('World'))\n```\n\nLet me know if you need something more specific!"
354
- if 'math' in msg_lower or 'equation' in msg_lower or 'calculate' in msg_lower:
355
- return "Here's a mathematical expression:\n\n$$E = mc^2$$\n\nThe energy $E$ of a body equals its mass $m$ times the speed of light $c$ squared."
356
- if 'table' in msg_lower:
357
- return "Here's a sample table:\n\n| Name | Age | Role |\n|------|-----|------|\n| Alice | 30 | Engineer |\n| Bob | 25 | Designer |\n| Carol | 35 | Manager |"
358
 
359
- return f"I understand you're asking about: *{message[:100]}*.\n\nI'm running in fallback mode. For full capabilities, connect an AI provider (OpenAI API key or Ollama).\n\n### Suggestions\n- Set `OPENAI_API_KEY` environment variable for GPT-4o responses\n- Run Ollama locally with `ollama pull llama3.2`\n- Ask me to write code, explain concepts, or help with problems"
360
-
361
- # ── Conversation Routes ─────────────────────────────────────────────────
362
-
363
- @app.route('/api/conversations', methods=['GET'])
364
- def list_conversations():
365
- search = request.args.get('search', '').strip()
366
- with get_db() as conn:
367
- if search:
368
- rows = conn.execute(
369
- 'SELECT * FROM conversations WHERE title LIKE ? ORDER BY pinned DESC, updated_at DESC LIMIT 50',
370
- (f'%{search}%',)
371
- ).fetchall()
372
- else:
373
- rows = conn.execute(
374
- 'SELECT * FROM conversations ORDER BY pinned DESC, updated_at DESC LIMIT 50'
375
- ).fetchall()
376
- return jsonify([dict(r) for r in rows])
377
 
378
- @app.route('/api/conversations', methods=['POST'])
379
- def create_conversation():
380
- data = request.json or {}
381
- title = data.get('title', 'New Chat')
382
- persona = data.get('persona', 'default')
383
- system_prompt = data.get('system_prompt', '')
384
- with get_db() as conn:
385
- cur = conn.execute(
386
- 'INSERT INTO conversations (title, persona, system_prompt) VALUES (?,?,?)',
387
- (title, persona, system_prompt)
388
- )
389
- conn.commit()
390
- row = conn.execute('SELECT * FROM conversations WHERE id=?', (cur.lastrowid,)).fetchone()
391
- return jsonify(dict(row)), 201
392
-
393
- @app.route('/api/conversations/<int:cid>', methods=['GET'])
394
- def get_conversation(cid):
395
- with get_db() as conn:
396
- conv = conn.execute('SELECT * FROM conversations WHERE id=?', (cid,)).fetchone()
397
- if not conv:
398
- return json_error('Conversation not found', 404)
399
- messages = conn.execute('SELECT * FROM messages WHERE conversation_id=? ORDER BY timestamp ASC', (cid,)).fetchall()
400
- return jsonify({'conversation': dict(conv), 'messages': [dict(m) for m in messages]})
401
-
402
- @app.route('/api/conversations/<int:cid>', methods=['PUT'])
403
- def update_conversation(cid):
404
- data = request.json or {}
405
- allowed = {'title', 'pinned', 'persona', 'system_prompt', 'model'}
406
- updates = {k: v for k, v in data.items() if k in allowed}
407
- if not updates:
408
- return json_error('No valid fields')
409
- set_clause = ', '.join(f'{k}=?' for k in updates)
410
- values = list(updates.values()) + [cid]
411
- with get_db() as conn:
412
- conn.execute(f'UPDATE conversations SET {set_clause}, updated_at=CURRENT_TIMESTAMP WHERE id=?', values)
413
- conn.commit()
414
- row = conn.execute('SELECT * FROM conversations WHERE id=?', (cid,)).fetchone()
415
- return jsonify(dict(row))
416
-
417
- @app.route('/api/conversations/<int:cid>', methods=['DELETE'])
418
- def delete_conversation(cid):
419
- with get_db() as conn:
420
- conn.execute('DELETE FROM messages WHERE conversation_id=?', (cid,))
421
- conn.execute('DELETE FROM conversations WHERE id=?', (cid,))
422
- conn.commit()
423
- return jsonify({'success': True})
424
-
425
- @app.route('/api/conversations/<int:cid>/title', methods=['POST'])
426
- def auto_title(cid):
427
- with get_db() as conn:
428
- first = conn.execute(
429
- 'SELECT content FROM messages WHERE conversation_id=? AND role="user" ORDER BY timestamp ASC LIMIT 1',
430
- (cid,)
431
- ).fetchone()
432
- if first:
433
- title = generate_title(first['content'])
434
- conn.execute('UPDATE conversations SET title=? WHERE id=?', (title, cid))
435
- conn.commit()
436
- return jsonify({'title': title})
437
- return jsonify({'title': 'New Chat'})
438
-
439
- # ── Feedback Route ──────────────────────────────────────────────────────
440
-
441
- @app.route('/api/feedback', methods=['POST'])
442
- def save_feedback():
443
- data = request.json or {}
444
- msg_id = data.get('message_id')
445
- rating = data.get('rating') # 1 or -1
446
- comment = data.get('comment', '')
447
- if not msg_id or rating not in (1, -1):
448
- return json_error('Invalid feedback')
449
- with get_db() as conn:
450
- conn.execute(
451
- 'INSERT OR REPLACE INTO feedback (message_id, rating, comment) VALUES (?,?,?)',
452
- (msg_id, rating, comment)
453
- )
454
- conn.execute('UPDATE messages SET feedback=?, feedback_text=? WHERE id=?', (rating, comment, msg_id))
455
  conn.commit()
456
- return jsonify({'success': True})
457
-
458
- # ── Export Route ─────────────────────────────────────────────────────────
459
-
460
- @app.route('/api/export', methods=['POST'])
461
- def export_chat():
462
- data = request.json or {}
463
- conv_id = data.get('conversation_id')
464
- if not conv_id:
465
- return json_error('No conversation_id')
466
- with get_db() as conn:
467
- conv = conn.execute('SELECT * FROM conversations WHERE id=?', (conv_id,)).fetchone()
468
- msgs = conn.execute('SELECT * FROM messages WHERE conversation_id=? ORDER BY timestamp ASC', (conv_id,)).fetchall()
469
- if not conv:
470
- return json_error('Not found', 404)
471
- export = {
472
- 'title': conv['title'],
473
- 'created': conv['created_at'],
474
- 'messages': [{'role': m['role'], 'content': m['content'], 'timestamp': m['timestamp']} for m in msgs]
475
- }
476
- return jsonify(export)
477
-
478
- # ── Existing Routes (preserved from original) ───────────────────────────
479
-
480
- @app.route('/')
481
- @app.route('/<path:path>')
482
- def serve_frontend(path=None):
483
- # Only serve index.html for frontend routes
484
- if path and path.startswith(('api/', 'uploads/', 'static/')):
485
- return json_error('Not found', 404)
486
- try:
487
- with open(os.path.join(BASE_DIR, 'index.html'), 'r', encoding='utf-8') as f:
488
- return Response(f.read(), mimetype='text/html')
489
- except Exception as e:
490
- logger.error(f'Failed to serve index.html: {e}')
491
- return 'AI Chat Backend is running.', 200
492
-
493
- @app.route('/api/status', methods=['GET'])
494
- def status():
495
- return jsonify({
496
- 'status': 'online',
497
- 'version': '2.0.0',
498
- 'timestamp': datetime.now().isoformat()
499
- })
500
-
501
- @app.route('/api/search', methods=['POST'])
502
- def search_api():
503
- data = request.json or {}
504
- query = data.get('query', '').strip()
505
- if not query:
506
- return json_error('No query')
507
- results = []
508
  try:
509
- from duckduckgo_search import DDGS
510
- with DDGS(timeout=10) as ddgs:
511
- for r in ddgs.text(query, max_results=5):
512
- results.append({'title': r['title'], 'href': r['href'], 'body': r['body']})
513
- except Exception as e:
514
- logger.warning(f'Search failed: {e}')
515
- return jsonify({'results': results, 'query': query})
516
-
517
- @app.route('/api/clear', methods=['POST'])
518
- def clear_all():
519
- with get_db() as conn:
520
- conn.execute('DELETE FROM messages')
521
- conn.execute('DELETE FROM conversations')
522
- conn.execute('DELETE FROM files')
523
- conn.execute('DELETE FROM feedback')
524
  conn.commit()
525
- return jsonify({'success': True})
526
-
527
- # ── Error Handlers ──────────────────────────────────────────────────────
528
-
529
- @app.errorhandler(404)
530
- def not_found(e):
531
- return jsonify({'error': 'Not found'}), 404
532
-
533
- @app.errorhandler(500)
534
- def server_error(e):
535
- return jsonify({'error': 'Internal server error'}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
 
537
  if __name__ == '__main__':
538
- port = int(os.environ.get('PORT', 7860))
539
- app.run(host='0.0.0.0', port=port, debug=False)
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory, render_template, Response, make_response
 
 
 
 
2
  from flask_cors import CORS
3
+ from flask_limiter import Limiter
4
+ from flask_limiter.util import get_remote_address
5
+ import os, json, uuid, datetime, sqlite3, time, random, re, logging, jwt, functools, requests
6
+ from werkzeug.utils import secure_filename
7
+ from datetime import datetime, timedelta
8
+
9
+ # Configuration
10
+ app = Flask(__name__)
11
+ CORS(app)
12
+ app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'nexa-secret-key-2026')
13
+ OPENROUTER_API_KEY = os.environ.get('OPENROUTER_API_KEY')
14
+ UPLOAD_FOLDER = 'uploads'
15
+ DATA_FOLDER = 'data'
16
+ GENERATED_FOLDER = 'generated'
17
+ ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'py', 'js', 'html', 'css', 'json', 'md', 'doc', 'docx', 'csv', 'xlsx'}
18
+
19
+ for folder in [UPLOAD_FOLDER, DATA_FOLDER, GENERATED_FOLDER]:
20
+ os.makedirs(folder, exist_ok=True)
21
+
22
+ # Logging
23
+ logging.basicConfig(level=logging.INFO)
24
  logger = logging.getLogger(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
+ # Rate Limiting
27
+ limiter = Limiter(
28
+ get_remote_address,
29
+ app=app,
30
+ default_limits=["10000 per day", "1000 per hour"],
31
+ storage_uri="memory://",
32
+ )
33
+
34
+ # Database Initialization
35
+ def get_db_connection():
36
+ conn = sqlite3.connect(os.path.join(DATA_FOLDER, 'chat.db'))
37
  conn.row_factory = sqlite3.Row
 
38
  return conn
39
 
40
  def init_db():
41
+ conn = get_db_connection()
42
+ c = conn.cursor()
43
+ c.execute('''CREATE TABLE IF NOT EXISTS users (
44
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ username TEXT UNIQUE NOT NULL,
46
+ email TEXT UNIQUE NOT NULL,
47
+ password TEXT NOT NULL,
48
+ avatar TEXT,
49
+ bio TEXT,
50
+ accent_color TEXT DEFAULT '#10b981',
51
+ settings TEXT,
52
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
53
+ )''')
54
+ c.execute('''CREATE TABLE IF NOT EXISTS conversations (
55
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ user_id INTEGER,
57
+ title TEXT,
58
+ model TEXT,
59
+ pinned INTEGER DEFAULT 0,
60
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
61
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
62
+ FOREIGN KEY (user_id) REFERENCES users (id)
63
+ )''')
64
+ c.execute('''CREATE TABLE IF NOT EXISTS messages (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ conversation_id INTEGER,
67
+ role TEXT,
68
+ content TEXT,
69
+ metadata TEXT,
70
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
71
+ FOREIGN KEY (conversation_id) REFERENCES conversations (id)
72
+ )''')
73
+ c.execute('''CREATE TABLE IF NOT EXISTS search_cache (
74
+ query TEXT PRIMARY KEY,
75
+ results TEXT,
76
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
77
+ )''')
78
+ conn.commit()
79
+ conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
  init_db()
82
 
83
+ # Auth Decorator
84
+ def token_required(f):
85
+ @functools.wraps(f)
86
+ def decorated(*args, **kwargs):
87
+ token = request.cookies.get('nexa_token')
88
+ if not token:
89
+ return jsonify({'message': 'Token is missing!'}), 401
90
+ try:
91
+ data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
92
+ current_user_id = data['user_id']
93
+ except:
94
+ return jsonify({'message': 'Token is invalid!'}), 401
95
+ return f(current_user_id, *args, **kwargs)
96
+ return decorated
97
+
98
+ # Helper Functions
99
  def allowed_file(filename):
100
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
101
 
102
+ def auto_select_emoji(text):
103
+ topic_map = {
104
+ r'code|python|js|html|css|programming|script': '💻',
105
+ r'data|chart|graph|statistic|analysis': '📊',
106
+ r'science|physics|biology|quantum': '🔬',
107
+ r'money|finance|stock|market|price': '💰',
108
+ r'news|search|current|latest|world': '📰',
109
+ r'art|image|draw|design|creative': '🎨',
110
+ r'math|calculation|formula': '🔢',
111
+ r'music|video|movie|media': '🎬'
112
+ }
113
+ for pattern, emoji in topic_map.items():
114
+ if re.search(pattern, text.lower()):
115
+ return emoji
116
+ return '💡'
117
 
118
+ # Routes
119
+ @app.route('/')
120
+ def index():
121
+ return send_from_directory('.', 'index.html')
122
 
123
+ @app.route('/api/health')
124
+ def health():
125
+ return jsonify({"status": "ok", "version": "2.0.0-pro", "timestamp": time.time()})
126
 
127
+ @app.route('/api/search', methods=['POST'])
128
+ def api_search():
129
+ data = request.json
130
+ query = data.get('query', '').strip()
131
+ if not query:
132
+ return jsonify({"error": "No query"}), 400
 
 
 
 
 
 
 
 
 
 
133
 
134
+ # Cache check
135
+ conn = get_db_connection()
136
+ cached = conn.execute('SELECT * FROM search_cache WHERE query = ?', (query,)).fetchone()
137
+ if cached:
138
+ timestamp = datetime.strptime(cached['timestamp'], '%Y-%m-%d %H:%M:%S')
139
+ if datetime.now() - timestamp < timedelta(minutes=5):
140
+ conn.close()
141
+ return jsonify(json.loads(cached['results']))
 
 
 
 
 
142
 
143
+ # Mock Search Results
144
+ results = {
145
+ "query": query,
146
+ "results": [
147
+ {"title": f"Latest news on {query}", "url": f"https://example.com/news/{uuid.uuid4().hex[:8]}", "snippet": f"This is a detailed summary of the most recent developments regarding {query} in 2026."},
148
+ {"title": f"Understanding {query}: A Complete Guide", "url": f"https://wiki-example.org/{query.replace(' ', '_')}", "snippet": f"A comprehensive overview covering the history, current state, and future outlook of {query}."},
149
+ {"title": f"{query} - Real-time Data and Analytics", "url": f"https://analytics.example.io/{uuid.uuid4().hex[:6]}", "snippet": f"Live tracking and statistical analysis for {query} with interactive visualizations."}
150
+ ],
151
+ "timestamp": datetime.now().isoformat()
152
+ }
153
 
154
+ conn.execute('INSERT OR REPLACE INTO search_cache (query, results, timestamp) VALUES (?, ?, CURRENT_TIMESTAMP)',
155
+ (query, json.dumps(results)))
156
+ conn.commit()
157
+ conn.close()
158
+ return jsonify(results)
159
+
160
+ @app.route('/api/chart', methods=['POST'])
161
+ def api_chart():
162
+ data = request.json
163
+ raw_data = data.get('data')
164
+ chart_type = data.get('type', 'bar')
165
+ title = data.get('title', 'Data Visualization')
166
 
167
+ # Generate Chart.js config
168
+ config = {
169
+ "type": chart_type,
170
+ "data": {
171
+ "labels": raw_data.get('labels', []),
172
+ "datasets": [{
173
+ "label": title,
174
+ "data": raw_data.get('values', []),
175
+ "backgroundColor": "rgba(16, 185, 129, 0.2)",
176
+ "borderColor": "#10b981",
177
+ "borderWidth": 2,
178
+ "tension": 0.4
179
+ }]
180
+ },
181
+ "options": {
182
+ "responsive": True,
183
+ "maintainAspectRatio": False,
184
+ "plugins": {
185
+ "legend": {"labels": {"color": "#e0e0e0"}},
186
+ "title": {"display": True, "text": title, "color": "#e0e0e0"}
187
+ },
188
+ "scales": {
189
+ "y": {"grid": {"color": "#333"}, "ticks": {"color": "#a0a0a0"}},
190
+ "x": {"grid": {"color": "#333"}, "ticks": {"color": "#a0a0a0"}}
191
+ }
192
+ }
193
+ }
194
+ return jsonify(config)
195
+
196
+ @app.route('/api/generate-image', methods=['POST'])
197
+ def api_generate_image():
198
+ data = request.json
199
+ prompt = data.get('prompt', 'A futuristic landscape')
200
+ # Simulate generation
201
+ image_id = str(uuid.uuid4())
202
+ # In a real app, you'd call an API here. Using pollinations for mock functionality.
203
+ image_url = f"https://image.pollinations.ai/prompt/{prompt.replace(' ', '%20')}?width=1024&height=1024&nologo=true"
204
+ return jsonify({"url": image_url, "prompt": prompt, "id": image_id})
205
+
206
+ @app.route('/api/generate-video', methods=['POST'])
207
+ def api_generate_video():
208
+ data = request.json
209
+ prompt = data.get('prompt', 'A flowing river in a forest')
210
+ # Simulate video generation
211
+ return jsonify({
212
+ "status": "success",
213
+ "video_url": "https://www.w3schools.com/html/mov_bbb.mp4", # Placeholder
214
+ "prompt": prompt
215
+ })
216
 
217
  @app.route('/upload', methods=['POST'])
218
  def upload_file():
219
  if 'file' not in request.files:
220
+ return jsonify({"error": "No file"}), 400
221
+ file = request.files['file']
222
+ if file and allowed_file(file.filename):
223
+ filename = secure_filename(f"{uuid.uuid4().hex}_{file.filename}")
224
+ filepath = os.path.join(UPLOAD_FOLDER, filename)
225
+ file.save(filepath)
226
+
227
+ # Mock text extraction
228
+ preview = "File content preview..."
229
+ if filename.endswith('.csv'):
230
+ preview = "col1,col2,col3\n10,20,30\n40,50,60"
231
+
232
+ return jsonify({
233
+ "success": True,
234
+ "filename": filename,
235
+ "original_name": file.filename,
236
+ "url": f"/uploads/{filename}",
237
+ "preview": preview
238
+ })
239
+ return jsonify({"error": "Invalid file type"}), 400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
  @app.route('/uploads/<filename>')
242
  def serve_upload(filename):
243
  return send_from_directory(UPLOAD_FOLDER, filename)
244
 
245
+ @app.route('/api/conversations', methods=['GET'])
246
+ @token_required
247
+ def get_conversations(current_user_id):
248
+ conn = get_db_connection()
249
+ convs = conn.execute('SELECT * FROM conversations WHERE user_id = ? ORDER BY pinned DESC, updated_at DESC', (current_user_id,)).fetchall()
250
+ conn.close()
251
+ return jsonify([dict(c) for c in convs])
 
 
 
 
 
252
 
253
+ @app.route('/api/conversations', methods=['POST'])
254
+ @token_required
255
+ def create_conversation(current_user_id):
256
+ data = request.json
257
+ title = data.get('title', 'New Conversation')
258
+ emoji = auto_select_emoji(title)
259
+ full_title = f"{emoji} {title}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
+ conn = get_db_connection()
262
+ cursor = conn.execute('INSERT INTO conversations (user_id, title) VALUES (?, ?)', (current_user_id, full_title))
263
+ conn.commit()
264
+ conv_id = cursor.lastrowid
265
+ conn.close()
266
+ return jsonify({"id": conv_id, "title": full_title})
267
 
268
  @app.route('/chat/stream', methods=['POST'])
269
+ @token_required
270
+ def chat_stream(current_user_id):
271
+ data = request.json
272
+ message = data.get('message')
273
  conv_id = data.get('conversation_id')
274
+ model = data.get('model', 'google/gemini-2.0-flash-exp:free')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
+ # Save user message
277
+ with get_db_connection() as conn:
278
+ conn.execute('INSERT INTO messages (conversation_id, role, content) VALUES (?, ?, ?)', (conv_id, 'user', message))
279
+ conn.commit()
280
+
281
  def generate():
282
+ if not OPENROUTER_API_KEY:
283
+ yield f"data: {json.dumps({'content': '⚠️ OpenRouter API Key is missing. Please add it to your Space secrets.'})}\n\n"
284
+ yield "data: [DONE]\n\n"
285
+ return
286
+
287
+ headers = {
288
+ "Authorization": f"Bearer {OPENROUTER_API_KEY}",
289
+ "HTTP-Referer": "https://huggingface.co/spaces",
290
+ "Content-Type": "application/json"
291
+ }
292
 
293
+ payload = {
294
+ "model": model,
295
+ "messages": [{"role": "user", "content": message}],
296
+ "stream": True
297
+ }
298
+
299
+ try:
300
+ response = requests.post(
301
+ "https://openrouter.ai/api/v1/chat/completions",
302
+ headers=headers,
303
+ json=payload,
304
+ stream=True
305
+ )
306
+
307
+ full_response = ""
308
+ for line in response.iter_lines():
309
+ if line:
310
+ line = line.decode('utf-8')
311
+ if line.startswith('data: '):
312
+ data_str = line[6:]
313
+ if data_str == '[DONE]': break
314
+
315
+ try:
316
+ chunk = json.loads(data_str)
317
+ content = chunk['choices'][0]['delta'].get('content', '')
318
+ if content:
319
+ full_response += content
320
+ yield f"data: {json.dumps({'content': content})}\n\n"
321
+ except: continue
322
+
323
+ # Save final assistant message
324
+ with get_db_connection() as conn:
325
+ conn.execute('INSERT INTO messages (conversation_id, role, content) VALUES (?, ?, ?)', (conv_id, 'assistant', full_response))
326
+ conn.execute('UPDATE conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', (conv_id,))
327
  conn.commit()
328
+
329
+ except Exception as e:
330
+ yield f"data: {json.dumps({'content': f'Error: {str(e)}'})}\n\n"
331
+
332
+ yield "data: [DONE]\n\n"
 
 
 
333
 
334
+ return Response(generate(), mimetype='text/event-stream')
335
+
336
+ @app.route('/chat', methods=['POST'])
337
+ @token_required
338
+ def chat(current_user_id):
339
+ data = request.json
340
+ message = data.get('message')
341
+ conv_id = data.get('conversation_id')
 
 
 
 
342
 
343
+ # Save user message
344
+ conn = get_db_connection()
345
+ conn.execute('INSERT INTO messages (conversation_id, role, content) VALUES (?, ?, ?)', (conv_id, 'user', message))
 
 
 
 
 
 
 
346
 
347
+ # Mock AI Response with autonomous feature detection
348
+ emoji = auto_select_emoji(message)
349
+ response_text = f"### {emoji} Assistant Response\n\nI've analyzed your request: '{message}'. "
 
 
 
 
 
 
 
 
 
 
350
 
351
+ # Simple Chart Trigger in Mock Response
352
+ if "chart" in message.lower() or "visualize" in message.lower():
353
+ response_text += "\n\nI've generated a visualization for you based on the data provided."
354
+
355
+ # Simple Image Trigger
356
+ if "image" in message.lower() or "generate" in message.lower():
357
+ response_text += "\n\nI've created an image visualization for your request."
 
 
 
 
 
 
 
 
 
 
 
358
 
359
+ with get_db_connection() as conn:
360
+ conn.execute('INSERT INTO messages (conversation_id, role, content) VALUES (?, ?, ?)', (conv_id, 'assistant', response_text))
361
+ conn.execute('UPDATE conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', (conv_id,))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  conn.commit()
363
+
364
+ return jsonify({"response": response_text})
365
+
366
+ @app.route('/api/profile', methods=['GET'])
367
+ @token_required
368
+ def get_profile(current_user_id):
369
+ conn = get_db_connection()
370
+ user = conn.execute('SELECT * FROM users WHERE id = ?', (current_user_id,)).fetchone()
371
+ conn.close()
372
+ if user:
373
+ return jsonify(dict(user))
374
+ return jsonify({"error": "User not found"}), 404
375
+
376
+ @app.route('/api/auth/signup', methods=['POST'])
377
+ def signup():
378
+ data = request.json
379
+ username = data.get('username', data.get('email', '').split('@')[0])
380
+ email = data.get('email')
381
+ password = data.get('password')
382
+
383
+ conn = get_db_connection()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  try:
385
+ conn.execute('INSERT INTO users (username, email, password) VALUES (?, ?, ?)', (username, email, password))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  conn.commit()
387
+ return jsonify({"success": True})
388
+ except:
389
+ return jsonify({"error": "User already exists"}), 400
390
+ finally:
391
+ conn.close()
392
+
393
+ @app.route('/api/auth/login', methods=['POST'])
394
+ def login():
395
+ data = request.json
396
+ email = data.get('email')
397
+ password = data.get('password')
398
+
399
+ conn = get_db_connection()
400
+ user = conn.execute('SELECT * FROM users WHERE email = ? AND password = ?', (email, password)).fetchone()
401
+ conn.close()
402
+
403
+ if user:
404
+ token = jwt.encode({'user_id': user['id'], 'exp': datetime.utcnow() + timedelta(days=7)}, app.config['SECRET_KEY'])
405
+ resp = make_response(jsonify({"success": True, "user": dict(user)}))
406
+ resp.set_cookie('nexa_token', token, httponly=True, samesite='Lax')
407
+ return resp
408
+ return jsonify({"error": "Invalid credentials"}), 401
409
+
410
+ @app.route('/api/auth/logout', methods=['POST'])
411
+ def logout():
412
+ resp = make_response(jsonify({"success": True}))
413
+ resp.set_cookie('nexa_token', '', expires=0)
414
+ return resp
415
 
416
  if __name__ == '__main__':
417
+ # Use port 7860 for Hugging Face Spaces
418
+ port = int(os.environ.get("PORT", 7860))
419
+ app.run(debug=False, host='0.0.0.0', port=port)