- Kiosk mode: fullscreen, wake lock, auto-login, context menu block, PWA/Capacitor detection - AI vision: camera photos analyzed by Gemma 3 27B vision model via OpenRouter - AI part classification: auto-suggest name/brand/category when entering part number - Public catalog chatbot: /api/chat endpoint with rate limiting, chat widget on catalog page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -249,6 +249,97 @@ def admin_js():
|
||||
def enhanced_search_js():
|
||||
return send_from_directory('.', 'enhanced-search.js')
|
||||
|
||||
@app.route('/chat-public.js')
|
||||
def chat_public_js():
|
||||
return send_from_directory('.', 'chat-public.js')
|
||||
|
||||
@app.route('/chat-public.css')
|
||||
def chat_public_css():
|
||||
return send_from_directory('.', 'chat-public.css')
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Public Chat Endpoint (rate limited, no auth)
|
||||
# ============================================================================
|
||||
|
||||
# Simple in-memory rate limiting for public chat
|
||||
_chat_rate = {} # ip -> [timestamps]
|
||||
|
||||
def _check_chat_rate(ip, max_per_min=10):
|
||||
"""Return True if request is allowed, False if rate limited."""
|
||||
import time
|
||||
now = time.time()
|
||||
if ip not in _chat_rate:
|
||||
_chat_rate[ip] = []
|
||||
# Clean old entries
|
||||
_chat_rate[ip] = [t for t in _chat_rate[ip] if now - t < 60]
|
||||
if len(_chat_rate[ip]) >= max_per_min:
|
||||
return False
|
||||
_chat_rate[ip].append(now)
|
||||
return True
|
||||
|
||||
@app.route('/api/chat', methods=['POST'])
|
||||
def public_chat():
|
||||
"""Public chatbot endpoint — searches the public catalog (no auth required)."""
|
||||
client_ip = request.remote_addr or '0.0.0.0'
|
||||
if not _check_chat_rate(client_ip):
|
||||
return jsonify({'error': 'Demasiadas solicitudes. Intenta en un minuto.'}), 429
|
||||
|
||||
body = request.get_json(force=True) if request.is_json else {}
|
||||
user_message = (body.get('message') or '').strip()
|
||||
if not user_message:
|
||||
return jsonify({'error': 'message required'}), 400
|
||||
|
||||
chat_history = body.get('history') or []
|
||||
|
||||
# Import AI chat service
|
||||
from services.ai_chat import chat as ai_chat_fn
|
||||
|
||||
ai_response = ai_chat_fn(user_message, chat_history)
|
||||
|
||||
search_results = []
|
||||
|
||||
try:
|
||||
search_query = ai_response.get('search_query')
|
||||
if search_query:
|
||||
session = Session()
|
||||
try:
|
||||
query_terms = [q.strip() for q in search_query.split('|') if q.strip()]
|
||||
for qt in query_terms[:3]:
|
||||
# Search parts in public catalog
|
||||
rows = session.execute(text("""
|
||||
SELECT p.id_part, p.oem_part_number, p.name_part,
|
||||
pg.name_part_group, pc.name_part_category
|
||||
FROM parts p
|
||||
LEFT JOIN part_groups pg ON p.group_id = pg.id_part_group
|
||||
LEFT JOIN part_categories pc ON pg.category_id = pc.id_part_category
|
||||
WHERE p.name_part ILIKE :q
|
||||
OR p.oem_part_number ILIKE :q
|
||||
OR pg.name_part_group ILIKE :q
|
||||
ORDER BY p.name_part
|
||||
LIMIT 5
|
||||
"""), {'q': f'%{qt}%'}).mappings().all()
|
||||
|
||||
for r in rows:
|
||||
search_results.append({
|
||||
'id_part': r['id_part'],
|
||||
'oem_part_number': r['oem_part_number'],
|
||||
'name_part': r['name_part'],
|
||||
'name_es': r['name_part_group'],
|
||||
'category': r['name_part_category'],
|
||||
'source': 'catalog'
|
||||
})
|
||||
finally:
|
||||
session.close()
|
||||
except Exception:
|
||||
pass # search failure is non-fatal
|
||||
|
||||
return jsonify({
|
||||
'response': ai_response.get('message', ''),
|
||||
'search_results': search_results,
|
||||
'vehicle': ai_response.get('vehicle')
|
||||
})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Public Catalog API — No auth required
|
||||
|
||||
Reference in New Issue
Block a user