| |
| |
|
|
| import os |
| import json |
| import re |
| import uuid |
| from flask import Flask, render_template, request, jsonify, send_file, Response, g |
| from flask_cors import CORS |
| from werkzeug.utils import secure_filename |
| import requests |
| from datetime import datetime |
| from io import BytesIO |
| from docx import Document |
| from docx.shared import Inches |
| import base64 |
| from binary_processor import BinaryProcessor |
| from docs import DocumentProcessor |
| import io |
|
|
| |
| |
| |
| from auth_middleware import require_auth, optional_auth, get_current_user_id, get_current_user_email, get_current_username, is_admin |
| from user_storage import ( |
| load_conversations as load_user_conversations, |
| save_conversations as save_user_conversations, |
| get_user_conversations_list, |
| load_workspace as load_user_workspace, |
| save_workspace as save_user_workspace, |
| load_user_profile, |
| save_user_profile, |
| update_user_last_active, |
| migrate_legacy_data |
| ) |
| from session_manager import create_session, validate_session, delete_session, hash_password, verify_password, generate_user_id, is_valid_email, is_valid_username |
|
|
| |
| |
| |
| if os.environ.get('SPACE_ID') or os.environ.get('HF_SPACE'): |
| UPLOAD_FOLDER = '/tmp/uploads' |
| INSTANCE_FOLDER = '/tmp/instance' |
| GENERATED_IMAGES_DIR = '/tmp/generated_images' |
| print("β Running on Hugging Face Spaces - using /tmp storage") |
| else: |
| UPLOAD_FOLDER = 'static/uploads' |
| INSTANCE_FOLDER = 'instance' |
| GENERATED_IMAGES_DIR = 'generated_images' |
| print("β Running locally - using static storage") |
|
|
| os.makedirs(UPLOAD_FOLDER, exist_ok=True) |
| os.makedirs(INSTANCE_FOLDER, exist_ok=True) |
| os.makedirs(GENERATED_IMAGES_DIR, exist_ok=True) |
|
|
| |
| from models import ( |
| query_ai_with_fallback, |
| generate_chat_title, |
| is_code_generation_request, |
| execute_python_code, |
| search_web, |
| extract_web_content, |
| analyze_image_with_ai, |
| call_pollinations_ai |
| ) |
|
|
| |
| from workspace import register_workspace_routes |
|
|
| |
| from image import FreeImageGenerator, create_free_image_generator |
|
|
| |
| from mydocs import DocumentCreator |
|
|
| from vision import get_vision_model |
|
|
| |
| from terminal import create_terminal_blueprint |
|
|
| app = Flask(__name__) |
| app.secret_key = os.urandom(24) |
| app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 |
| app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 |
| app.config['TIMEOUT'] = None |
| CORS(app) |
|
|
| |
| app.register_blueprint(create_terminal_blueprint(app)) |
|
|
| binary_processor = BinaryProcessor() |
|
|
| |
| |
| image_generator = FreeImageGenerator(output_dir=GENERATED_IMAGES_DIR) |
|
|
| |
| |
| |
|
|
| def get_user_conversations_for_current_user(): |
| """Get conversations for the current authenticated user""" |
| user_id = get_current_user_id() |
| if user_id: |
| return load_user_conversations(user_id) |
| return {} |
|
|
|
|
| def save_user_conversations_for_current_user(conversations): |
| """Save conversations for the current authenticated user""" |
| user_id = get_current_user_id() |
| if user_id: |
| return save_user_conversations(user_id, conversations) |
| return False |
|
|
|
|
| def get_user_image_generator_for_current_user(): |
| """Get image generator for the current user""" |
| user_id = get_current_user_id() |
| if user_id: |
| return create_free_image_generator(user_id=user_id) |
| return image_generator |
|
|
| |
| ALLOWED_EXTENSIONS = { |
| 'txt', 'md', 'py', 'js', 'html', 'css', 'json', 'xml', 'csv', |
| 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', |
| 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp', |
| 'mp3', 'wav', 'ogg', 'flac', 'm4a', |
| 'mp4', 'avi', 'mov', 'mkv', 'webm', |
| 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', |
| 'exe', 'dll', 'so', 'dylib', |
| 'db', 'sqlite', 'sqlite3', 'sql', |
| 'ttf', 'otf', 'woff', 'woff2', |
| 'java', 'c', 'cpp', 'h', 'rb', 'php', 'go', 'rs', 'swift', 'kt' |
| } |
|
|
| |
| |
| |
|
|
| def generate_message_id(): |
| """Generate a unique ID for a message""" |
| return str(uuid.uuid4()) |
|
|
| def sanitize_filename(text): |
| if not text: |
| return "New Chat" |
| text = ''.join(char for char in text if char.isprintable() and char not in '\n\r\t') |
| replacements = {'/': '-', '\\': '-', ':': '-', '*': '-', '?': '-', '"': "'", '<': '-', '>': '-', '|': '-'} |
| for old, new in replacements.items(): |
| text = text.replace(old, new) |
| text = text.strip()[:100] |
| return text if text else "New Chat" |
|
|
| def allowed_file(filename): |
| return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS |
|
|
| |
| |
| |
|
|
| |
| def load_conversations_legacy(): |
| """Legacy function - use get_user_conversations_for_current_user() instead""" |
| try: |
| if os.path.exists(CONVERSATIONS_FILE): |
| with open(CONVERSATIONS_FILE, 'r', encoding='utf-8') as f: |
| conversations = json.load(f) |
| for conv_id, conv_data in conversations.items(): |
| if 'versions' not in conv_data: |
| conv_data['versions'] = {} |
| if 'current_version_index' not in conv_data: |
| conv_data['current_version_index'] = {} |
| if 'branch_root' not in conv_data: |
| conv_data['branch_root'] = None |
| if 'version_branches' not in conv_data: |
| conv_data['version_branches'] = {} |
| if 'pending_attachments' not in conv_data: |
| conv_data['pending_attachments'] = [] |
| return conversations |
| except Exception as e: |
| print(f"Error loading conversations: {e}") |
| return {} |
|
|
| def save_conversations_legacy(conversations): |
| """Legacy function - use save_user_conversations_for_current_user() instead""" |
| try: |
| with open(CONVERSATIONS_FILE, 'w', encoding='utf-8') as f: |
| json.dump(conversations, f, indent=2, ensure_ascii=False) |
| except Exception as e: |
| print(f"Error saving conversations: {e}") |
|
|
| def extract_text_from_file(file_content, filename): |
| try: |
| processed_output = binary_processor.process_file(file_content, filename) |
| if len(processed_output) > 50000: |
| processed_output = processed_output[:50000] + "\n\n[Content truncated due to size]" |
| return processed_output |
| except Exception as e: |
| print(f"Error in enhanced extraction: {e}") |
| return f"[Error extracting from {filename}: {str(e)}]" |
|
|
| def export_to_word(conversation_data): |
| doc = Document() |
| doc.add_heading(f'HenAi Chat: {conversation_data["title"]}', 0) |
| doc.add_paragraph(f'Exported on: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}') |
| doc.add_paragraph('-' * 50) |
|
|
| for msg in conversation_data['messages']: |
| if msg['role'] == 'user': |
| doc.add_heading('You:', level=2) |
| else: |
| doc.add_heading('HenAi:', level=2) |
|
|
| content = msg['content'] |
| code_blocks = re.findall(r'```(\w+)?\n([\s\S]*?)```', content) |
| if code_blocks: |
| parts = re.split(r'```\w*\n[\s\S]*?```', content) |
| for i, part in enumerate(parts): |
| if part.strip(): |
| doc.add_paragraph(part.strip()) |
| if i < len(code_blocks): |
| lang, code = code_blocks[i] |
| doc.add_paragraph(f'[{lang.upper()} Code]') |
| doc.add_paragraph(code.strip()) |
| else: |
| doc.add_paragraph(content) |
| doc.add_paragraph('') |
|
|
| return doc |
|
|
| def is_image_generation_request(message): |
| if not message: |
| return False |
| |
| message_lower = message.lower() |
| |
| analysis_keywords = [ |
| 'analyze', 'analyse', 'what is', 'what\'s', 'tell me about', |
| 'describe', 'explain', 'read this', 'look at', 'examine', |
| 'extract text', 'ocr', 'recognize', 'identify' |
| ] |
| |
| for keyword in analysis_keywords: |
| if keyword in message_lower: |
| return False |
| |
| image_keywords = [ |
| 'generate image', 'create image', 'make image', 'draw image', |
| 'generate picture', 'create picture', 'make picture', 'draw picture', |
| 'generate photo', 'create photo', 'make photo', |
| 'generate art', 'create art', 'make art', |
| 'generate illustration', 'create illustration', |
| 'ai image', 'ai art', 'ai generate', |
| 'stable diffusion', 'dall-e', 'midjourney', |
| 'flux', 'sdxl', 'sd1.5', |
| 'image of', 'picture of', 'photo of', |
| 'draw me', 'generate me', 'create me', |
| 'make an image', 'make a picture', 'generate an image' |
| ] |
| |
| for keyword in image_keywords: |
| if keyword in message_lower: |
| return True |
| |
| words = message_lower.split() |
| if len(words) <= 5 and any(word in ['image', 'picture', 'photo', 'art'] for word in words): |
| if not any(word in message_lower for word in ['this', 'the', 'that', 'attached']): |
| return True |
| |
| return False |
|
|
| def extract_image_prompt(message): |
| message_lower = message.lower() |
| |
| prefixes = [ |
| 'generate image of', 'create image of', 'make image of', 'draw image of', |
| 'generate picture of', 'create picture of', 'make picture of', 'draw picture of', |
| 'generate photo of', 'create photo of', 'make photo of', |
| 'generate art of', 'create art of', 'make art of', |
| 'ai image of', 'ai art of', |
| 'image of', 'picture of', 'photo of', |
| 'draw me', 'generate me', 'create me', |
| 'generate an image of', 'create an image of', 'make an image of', |
| 'generate a picture of', 'create a picture of', 'make a picture of' |
| ] |
| |
| prompt = message.strip() |
| for prefix in prefixes: |
| if message_lower.startswith(prefix): |
| prompt = message[len(prefix):].strip() |
| break |
| |
| prompt = prompt.strip('.,!?;:') |
| |
| if len(prompt) < 3: |
| prompt = message.strip() |
| |
| return prompt |
|
|
| |
| |
| |
|
|
| |
| |
| |
|
|
| @app.route('/api/auth/register', methods=['POST']) |
| def api_register(): |
| """Register a new user""" |
| data = request.json |
| email = data.get('email', '').strip().lower() |
| username = data.get('username', '').strip() |
| password = data.get('password', '') |
| |
| |
| if not email or not username or not password: |
| return jsonify({'success': False, 'error': 'All fields are required'}), 400 |
| |
| if not is_valid_email(email): |
| return jsonify({'success': False, 'error': 'Invalid email format'}), 400 |
| |
| if not is_valid_username(username): |
| return jsonify({'success': False, 'error': 'Username must be 3-30 characters (letters, numbers, underscore)'}), 400 |
| |
| if len(password) < 6: |
| return jsonify({'success': False, 'error': 'Password must be at least 6 characters'}), 400 |
| |
| |
| user_dir = f'/data/users' |
| if os.path.exists(user_dir): |
| for uid in os.listdir(user_dir): |
| profile_path = os.path.join(user_dir, uid, 'profile.json') |
| if os.path.exists(profile_path): |
| try: |
| with open(profile_path, 'r') as f: |
| profile = json.load(f) |
| if profile.get('email') == email: |
| return jsonify({'success': False, 'error': 'This email is already registered. Please use a different email or login.'}), 400 |
| if profile.get('username') == username: |
| return jsonify({'success': False, 'error': f'Username "{username}" is already taken. Please choose a different username.'}), 400 |
| except: |
| pass |
| |
| |
| user_id = generate_user_id() |
| hashed_password, salt = hash_password(password) |
| |
| |
| profile = { |
| 'user_id': user_id, |
| 'username': username, |
| 'email': email, |
| 'password_hash': hashed_password, |
| 'password_salt': salt, |
| 'created_at': datetime.now().isoformat(), |
| 'last_active': datetime.now().isoformat(), |
| 'theme': 'dark', |
| 'avatar': None |
| } |
| |
| |
| user_dir_path = f'/data/users/{user_id}' |
| os.makedirs(user_dir_path, exist_ok=True) |
| profile_path = os.path.join(user_dir_path, 'profile.json') |
| with open(profile_path, 'w') as f: |
| json.dump(profile, f, indent=2) |
| |
| |
| conversations_path = os.path.join(user_dir_path, 'conversations.json') |
| with open(conversations_path, 'w') as f: |
| json.dump({}, f) |
| |
| |
| workspace_path = os.path.join(user_dir_path, 'workspace.json') |
| with open(workspace_path, 'w') as f: |
| json.dump({"folders": []}, f) |
| |
| |
| session_data = create_session(user_id) |
| |
| return jsonify({ |
| 'success': True, |
| 'user_id': user_id, |
| 'username': username, |
| 'email': email, |
| 'session_token': session_data['session_token'] |
| }) |
|
|
|
|
| @app.route('/api/auth/login', methods=['POST']) |
| def api_login(): |
| """Login user""" |
| data = request.json |
| email = data.get('email', '').strip().lower() |
| password = data.get('password', '') |
| |
| if not email or not password: |
| return jsonify({'success': False, 'error': 'Email and password required'}), 400 |
| |
| |
| user_dir = '/data/users' |
| if not os.path.exists(user_dir): |
| return jsonify({'success': False, 'error': 'Invalid credentials'}), 401 |
| |
| found_user = None |
| found_user_id = None |
| |
| for user_id in os.listdir(user_dir): |
| profile_path = os.path.join(user_dir, user_id, 'profile.json') |
| if os.path.exists(profile_path): |
| try: |
| with open(profile_path, 'r') as f: |
| profile = json.load(f) |
| if profile.get('email') == email: |
| found_user = profile |
| found_user_id = user_id |
| break |
| except: |
| continue |
| |
| if not found_user: |
| return jsonify({'success': False, 'error': 'Invalid credentials'}), 401 |
| |
| |
| stored_hash = found_user.get('password_hash') |
| stored_salt = found_user.get('password_salt') |
| |
| if not stored_hash or not stored_salt: |
| return jsonify({'success': False, 'error': 'Invalid credentials'}), 401 |
| |
| if not verify_password(password, stored_hash, stored_salt): |
| return jsonify({'success': False, 'error': 'Invalid credentials'}), 401 |
| |
| |
| found_user['last_active'] = datetime.now().isoformat() |
| profile_path = os.path.join(user_dir, found_user_id, 'profile.json') |
| with open(profile_path, 'w') as f: |
| json.dump(found_user, f, indent=2) |
| |
| |
| session_data = create_session(found_user_id) |
| |
| return jsonify({ |
| 'success': True, |
| 'user_id': found_user_id, |
| 'username': found_user.get('username'), |
| 'email': found_user.get('email'), |
| 'session_token': session_data['session_token'] |
| }) |
|
|
|
|
| @app.route('/api/auth/verify_session', methods=['POST']) |
| def api_verify_session(): |
| """Verify a session token""" |
| data = request.json |
| session_token = data.get('session_token', '') |
| user_id = data.get('user_id', '') |
| |
| if not session_token: |
| return jsonify({'success': False, 'error': 'Session token required'}), 400 |
| |
| validated_user_id = validate_session(session_token) |
| |
| if validated_user_id and (not user_id or validated_user_id == user_id): |
| return jsonify({'success': True, 'user_id': validated_user_id}) |
| |
| return jsonify({'success': False, 'error': 'Invalid session'}), 401 |
|
|
|
|
| @app.route('/api/auth/logout', methods=['POST']) |
| def api_logout(): |
| """Logout user - invalidate session""" |
| data = request.json |
| session_token = data.get('session_token', '') |
| |
| if session_token: |
| delete_session(session_token) |
| |
| return jsonify({'success': True}) |
|
|
|
|
| @app.route('/api/auth/reset_password', methods=['POST']) |
| def api_reset_password(): |
| """Reset user password""" |
| data = request.json |
| email = data.get('email', '').strip().lower() |
| new_password = data.get('new_password', '') |
| |
| if not email or not new_password: |
| return jsonify({'success': False, 'error': 'Email and new password required'}), 400 |
| |
| if len(new_password) < 6: |
| return jsonify({'success': False, 'error': 'Password must be at least 6 characters'}), 400 |
| |
| |
| user_dir = '/data/users' |
| if not os.path.exists(user_dir): |
| return jsonify({'success': False, 'error': 'User not found'}), 404 |
| |
| found_user_id = None |
| found_profile = None |
| |
| for user_id in os.listdir(user_dir): |
| profile_path = os.path.join(user_dir, user_id, 'profile.json') |
| if os.path.exists(profile_path): |
| try: |
| with open(profile_path, 'r') as f: |
| profile = json.load(f) |
| if profile.get('email') == email: |
| found_user_id = user_id |
| found_profile = profile |
| break |
| except: |
| continue |
| |
| if not found_user_id: |
| return jsonify({'success': False, 'error': 'User not found'}), 404 |
| |
| |
| new_hash, new_salt = hash_password(new_password) |
| found_profile['password_hash'] = new_hash |
| found_profile['password_salt'] = new_salt |
| |
| profile_path = os.path.join(user_dir, found_user_id, 'profile.json') |
| with open(profile_path, 'w') as f: |
| json.dump(found_profile, f, indent=2) |
| |
| return jsonify({'success': True}) |
|
|
|
|
| @app.route('/api/auth/google', methods=['POST']) |
| def api_google_auth(): |
| """Google OAuth login/register""" |
| data = request.json |
| email = data.get('email', '').strip().lower() |
| name = data.get('name', '').strip() |
| google_id = data.get('google_id', '') |
| |
| if not email: |
| return jsonify({'success': False, 'error': 'Email required'}), 400 |
| |
| |
| user_dir = '/data/users' |
| found_user_id = None |
| found_profile = None |
| |
| if os.path.exists(user_dir): |
| for user_id in os.listdir(user_dir): |
| profile_path = os.path.join(user_dir, user_id, 'profile.json') |
| if os.path.exists(profile_path): |
| try: |
| with open(profile_path, 'r') as f: |
| profile = json.load(f) |
| if profile.get('email') == email: |
| found_user_id = user_id |
| found_profile = profile |
| break |
| except: |
| continue |
| |
| if not found_user_id: |
| |
| user_id = generate_user_id() |
| username = name.replace(' ', '_').lower()[:30] |
| |
| base_username = username |
| counter = 1 |
| while True: |
| taken = False |
| for uid in os.listdir(user_dir) if os.path.exists(user_dir) else []: |
| profile_path = os.path.join(user_dir, uid, 'profile.json') |
| if os.path.exists(profile_path): |
| try: |
| with open(profile_path, 'r') as f: |
| profile = json.load(f) |
| if profile.get('username') == username: |
| taken = True |
| break |
| except: |
| continue |
| if not taken: |
| break |
| username = f"{base_username}{counter}" |
| counter += 1 |
| |
| profile = { |
| 'user_id': user_id, |
| 'username': username, |
| 'email': email, |
| 'google_id': google_id, |
| 'created_at': datetime.now().isoformat(), |
| 'last_active': datetime.now().isoformat(), |
| 'theme': 'dark', |
| 'avatar': None |
| } |
| |
| user_dir_path = f'/data/users/{user_id}' |
| os.makedirs(user_dir_path, exist_ok=True) |
| profile_path = os.path.join(user_dir_path, 'profile.json') |
| with open(profile_path, 'w') as f: |
| json.dump(profile, f, indent=2) |
| |
| |
| conversations_path = os.path.join(user_dir_path, 'conversations.json') |
| with open(conversations_path, 'w') as f: |
| json.dump({}, f) |
| |
| workspace_path = os.path.join(user_dir_path, 'workspace.json') |
| with open(workspace_path, 'w') as f: |
| json.dump({"folders": []}, f) |
| |
| found_user_id = user_id |
| found_profile = profile |
| else: |
| |
| found_profile['last_active'] = datetime.now().isoformat() |
| if google_id and not found_profile.get('google_id'): |
| found_profile['google_id'] = google_id |
| profile_path = os.path.join(user_dir, found_user_id, 'profile.json') |
| with open(profile_path, 'w') as f: |
| json.dump(found_profile, f, indent=2) |
| |
| |
| session_data = create_session(found_user_id) |
| |
| return jsonify({ |
| 'success': True, |
| 'user_id': found_user_id, |
| 'username': found_profile.get('username'), |
| 'email': found_profile.get('email'), |
| 'session_token': session_data['session_token'] |
| }) |
|
|
| @app.route('/') |
| @optional_auth |
| def index(): |
| return render_template('index.html') |
|
|
| @app.route('/api/chat', methods=['POST']) |
| @require_auth |
| def chat(): |
| data = request.json |
| message = data.get('message', '') |
| conversation_id = data.get('conversation_id') |
| regenerate = data.get('regenerate', False) |
| regenerate_from = data.get('regenerate_from') |
| attached_files = data.get('attached_files', []) |
| |
| |
| user_id = get_current_user_id() |
| conversations = get_user_conversations_for_current_user() |
|
|
| |
| if not conversation_id or conversation_id not in conversations: |
| conversation_id = str(uuid.uuid4()) |
| conversations[conversation_id] = { |
| 'id': conversation_id, |
| 'messages': [], |
| 'title': 'New Chat', |
| 'created_at': datetime.now().isoformat(), |
| 'last_updated': datetime.now().isoformat(), |
| 'versions': {}, |
| 'current_version_index': {}, |
| 'branch_root': None, |
| 'version_branches': {}, |
| 'pending_attachments': [] |
| } |
|
|
| conv = conversations[conversation_id] |
|
|
| for key in ['versions', 'current_version_index', 'version_branches', 'pending_attachments']: |
| if key not in conv: |
| conv[key] = {} if key != 'pending_attachments' else [] |
|
|
| |
| if conv.get('pending_attachments'): |
| if message: |
| if not attached_files: |
| attached_files = [] |
| |
| for pending_file in conv['pending_attachments']: |
| attached_files.append({ |
| 'name': pending_file['name'], |
| 'content': pending_file['content'] |
| }) |
| |
| conv['pending_attachments'] = [] |
| save_user_conversations_for_current_user(conversations) |
|
|
| |
| full_message = message |
| ai_message = message |
| display_message = message |
| |
| if attached_files: |
| file_contexts = [] |
| file_names = [] |
| for file_info in attached_files: |
| file_names.append(file_info['name']) |
| |
| file_contexts.append(f"\n\n--- BEGIN FILE: {file_info['name']} ---\n{file_info['content']}\n--- END FILE: {file_info['name']} ---\n") |
| |
| ai_message = message + ''.join(file_contexts) |
| file_list = ", ".join(file_names) |
| display_message = message + (f"\n\nπ **Attached files:** {file_list}" if message else f"π **Attached files:** {file_list}") |
| full_message = display_message |
| else: |
| ai_message = message |
| full_message = message |
|
|
| response_data = { |
| 'response': '', |
| 'code_execution': None, |
| 'conversation_id': conversation_id, |
| 'title': conv['title'], |
| 'versions': conv.get('versions', {}), |
| 'current_version_index': conv.get('current_version_index', {}) |
| } |
|
|
| |
| |
| |
| if regenerate and regenerate_from is not None: |
| msg_key = str(regenerate_from) |
|
|
| if msg_key not in conv['versions']: |
| conv['versions'][msg_key] = [] |
| |
| |
| if regenerate_from < len(conv['messages']): |
| current_response = conv['messages'][regenerate_from]['content'] |
| current_version_idx = conv['current_version_index'].get(msg_key, 0) |
| |
| if current_response not in conv['versions'][msg_key]: |
| conv['versions'][msg_key].append(current_response) |
| conv['current_version_index'][msg_key] = len(conv['versions'][msg_key]) - 1 |
| current_version_idx = len(conv['versions'][msg_key]) - 1 |
| |
| |
| subsequent_messages = conv['messages'][regenerate_from + 1:] |
| if subsequent_messages: |
| version_branch_key = f"{msg_key}_v{current_version_idx}" |
| conv['version_branches'][version_branch_key] = subsequent_messages |
|
|
| |
| if regenerate_from > 0 and regenerate_from - 1 < len(conv['messages']): |
| conv['messages'] = conv['messages'][:regenerate_from] |
| conv['branch_root'] = regenerate_from - 1 |
| |
| |
| new_version_idx = len(conv['versions'].get(msg_key, [])) |
| conv['current_version_index'][msg_key] = new_version_idx |
| |
| |
| indices_to_remove = [] |
| for idx_key in list(conv.get('current_version_index', {}).keys()): |
| try: |
| if int(idx_key) >= regenerate_from: |
| indices_to_remove.append(idx_key) |
| except ValueError: |
| continue |
| |
| for idx_key in indices_to_remove: |
| del conv['current_version_index'][idx_key] |
|
|
| |
| context = [] |
| for msg in conv['messages']: |
| if msg['role'] == 'user' and msg.get('attachments'): |
| if 'ai_content' in msg: |
| context.append({"role": msg['role'], "content": msg['ai_content']}) |
| else: |
| user_content = msg['content'] |
| if 'attachments' in msg and msg['attachments']: |
| file_context = "" |
| for file_info in msg['attachments']: |
| if 'content' in file_info: |
| file_context += file_info['content'] |
| if file_context: |
| user_content = msg['content'] + file_context |
| context.append({"role": msg['role'], "content": user_content}) |
| else: |
| context.append({"role": msg['role'], "content": msg['content']}) |
| |
| is_code_gen = is_code_generation_request(full_message) |
| prompt_for_ai = ai_message |
| |
| if is_code_gen: |
| prompt_for_ai += "\n\nIMPORTANT: Provide the COMPLETE, FULLY FUNCTIONAL code with at least 500 lines. Do not abbreviate or use placeholders." |
|
|
| |
| has_attachments_in_user = False |
| if regenerate_from > 0 and regenerate_from - 1 < len(conv['messages']): |
| user_msg = conv['messages'][regenerate_from - 1] |
| if user_msg.get('attachments') and len(user_msg.get('attachments', [])) > 0: |
| has_attachments_in_user = True |
| print(f"π Regeneration: User message had attachments - forcing OpenRouter only") |
| |
| if has_attachments_in_user: |
| from models import query_openrouter |
| ai_response = query_openrouter(prompt_for_ai, context, is_code_gen) |
| else: |
| |
| ai_response = query_ai_with_fallback(prompt_for_ai, context, is_code_gen) |
| |
| response_data['response'] = ai_response |
|
|
| |
| conv['messages'].append({ |
| 'role': 'assistant', |
| 'content': response_data['response'], |
| 'timestamp': datetime.now().isoformat(), |
| 'regenerated': True, |
| 'version': new_version_idx + 1 |
| }) |
|
|
| |
| if response_data['response'] not in conv['versions'][msg_key]: |
| conv['versions'][msg_key].append(response_data['response']) |
| conv['current_version_index'][msg_key] = len(conv['versions'][msg_key]) - 1 |
|
|
| |
| |
| |
| elif full_message.lower().startswith('/search'): |
| query = full_message[7:].strip() |
| if not query: |
| response_data['response'] = "Please provide a search query. Example: `/search artificial intelligence`" |
| else: |
| response_data['response'] = search_web(query) |
|
|
| elif full_message.lower().startswith('/extract'): |
| url = full_message[8:].strip() |
| if not url: |
| response_data['response'] = "Please provide a URL. Example: `/extract https://example.com`" |
| else: |
| response_data['response'] = extract_web_content(url) |
|
|
| elif full_message.lower().startswith('/code'): |
| code = full_message[5:].strip() |
| if not code: |
| response_data['response'] = "Please provide Python code. Example: `/code print('Hello World!')`" |
| else: |
| exec_result = execute_python_code(code) |
| response_data['code_execution'] = exec_result |
| if exec_result['success']: |
| response_data['response'] = f"β
**Code executed successfully!**\n\n```\n{exec_result['output']}\n```" |
| else: |
| response_data['response'] = f"β **Code execution failed!**\n\n```\n{exec_result['error']}\n```" |
|
|
| elif full_message.lower().startswith('/generate') or full_message.lower().startswith('/image'): |
| if full_message.lower().startswith('/generate'): |
| image_prompt = full_message[9:].strip() |
| else: |
| image_prompt = full_message[6:].strip() |
| |
| if not image_prompt: |
| response_data['response'] = "Please provide an image description. Example: `/generate a beautiful sunset over mountains`" |
| else: |
| try: |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
| filename = f"ai_gen_{timestamp}.png" |
| |
| |
| user_image_gen = get_user_image_generator_for_current_user() |
| |
| try: |
| image_path = user_image_gen.generate_huggingface(image_prompt, output_name=filename) |
| except Exception as e: |
| print(f"Hugging Face generation failed: {e}, falling back to local generation") |
| try: |
| image_path = user_image_gen.generate_local_sd(image_prompt, output_name=filename) |
| except Exception as e2: |
| print(f"Local generation also failed: {e2}") |
| raise e |
| |
| image_filename = os.path.basename(image_path) |
| |
| |
| image_html = f''' |
| <div style="text-align: center; margin: 15px 0;"> |
| <img src="/api/generated_image/{image_filename}" |
| alt="Generated: {image_prompt}" |
| style="max-width: 100%; max-height: 400px; border-radius: 12px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.3);" |
| onclick="window.open('/api/generated_image/{image_filename}', '_blank')"> |
| <div style="margin-top: 8px; font-size: 0.75rem; color: var(--text-muted);"> |
| <i class="fas fa-expand"></i> Click to view full size |
| </div> |
| </div> |
| ''' |
| response_data['response'] = f"π¨ **Generated Image: \"{image_prompt}\"**\n\n{image_html}" |
| response_data['is_image_generation'] = True |
| response_data['image_path'] = image_path |
| response_data['image_prompt'] = image_prompt |
| |
| except Exception as e: |
| print(f"Image generation error: {e}") |
| response_data['response'] = f"β **Image generation failed!**\n\nError: {str(e)}\n\nPlease try a different prompt." |
|
|
| elif full_message.lower().startswith('/help'): |
| response_data['response'] = """**π HenAi Commands & Features** |
| |
| **Commands:** |
| β’ `/search <query>` - Search the web |
| β’ `/extract <url>` - Extract content from a URL |
| β’ `/code <python>` - Execute Python code |
| β’ `/generate <description>` or `/image <description>` - Generate AI images (free!) |
| β’ `/help` - Show this help |
| |
| **Message Actions:** |
| β’ π **Copy** - Click copy on any message |
| β’ βοΈ **Edit** - Click edit on your messages (creates new branch) |
| β’ π **Regenerate** - Get new responses (creates sibling branch) |
| β’ βοΈ **Toggle Versions** - Switch between chat versions |
| |
| **Features:** |
| β’ π Multiple file attachments |
| β’ πΎ Auto-save conversations |
| β’ π·οΈ Auto-titled chats |
| β’ βοΈ Rename chats |
| β’ π₯ Export chats to Word |
| β’ π Version history with branching |
| β’ π¨ Free AI Image Generation""" |
|
|
| elif is_image_generation_request(full_message) and not attached_files: |
| image_prompt = extract_image_prompt(full_message) |
| |
| try: |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
| filename = f"ai_gen_{timestamp}.png" |
| |
| |
| user_image_gen = get_user_image_generator_for_current_user() |
| |
| try: |
| image_path = user_image_gen.generate_huggingface(image_prompt, output_name=filename) |
| except Exception as e: |
| print(f"Hugging Face generation failed: {e}, falling back to local generation") |
| try: |
| image_path = user_image_gen.generate_local_sd(image_prompt, output_name=filename) |
| except Exception as e2: |
| print(f"Local generation also failed: {e2}") |
| raise e |
| |
| image_filename = os.path.basename(image_path) |
| |
| image_html = f''' |
| <div style="text-align: center; margin: 15px 0;"> |
| <img src="/api/generated_image/{image_filename}" |
| alt="Generated: {image_prompt}" |
| style="max-width: 100%; max-height: 400px; border-radius: 12px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.3);" |
| onclick="window.open('/api/generated_image/{image_filename}', '_blank')"> |
| <div style="margin-top: 8px; font-size: 0.75rem; color: var(--text-muted);"> |
| <i class="fas fa-expand"></i> Click to view full size |
| </div> |
| </div> |
| ''' |
| response_data['response'] = f"π¨ **Here's an image I generated for you:**\n\n{image_html}" |
| response_data['is_image_generation'] = True |
| response_data['image_path'] = image_path |
| response_data['image_prompt'] = image_prompt |
| |
| except Exception as e: |
| print(f"Image generation error: {e}") |
| response_data['response'] = f"β **Image generation failed!**\n\nError: {str(e)}\n\nPlease try a different prompt." |
|
|
| |
| |
| |
| else: |
| |
| context = [] |
| for msg in conv['messages']: |
| if msg['role'] == 'user' and msg.get('attachments'): |
| if 'ai_content' in msg: |
| context.append({"role": msg['role'], "content": msg['ai_content']}) |
| else: |
| user_content = msg['content'] |
| if 'attachments' in msg and msg['attachments']: |
| file_context = "" |
| for file_info in msg['attachments']: |
| if 'content' in file_info: |
| file_context += file_info['content'] |
| if file_context: |
| user_content = msg['content'] + file_context |
| context.append({"role": msg['role'], "content": user_content}) |
| else: |
| context.append({"role": msg['role'], "content": msg['content']}) |
| |
| print(f"Context messages count: {len(context)}") |
| |
| is_code_gen = is_code_generation_request(full_message) |
| print(f"Code generation detection: {is_code_gen}") |
|
|
| prompt_for_ai = ai_message |
| |
| if is_code_gen: |
| prompt_for_ai += "\n\nIMPORTANT: Provide the COMPLETE, FULLY FUNCTIONAL code with at least 500 lines. Do not abbreviate or use placeholders." |
|
|
| |
| has_attachments = attached_files and len(attached_files) > 0 |
| if has_attachments: |
| print(f"π Attachments detected ({len(attached_files)} files) - forcing OpenRouter only (no Pollinations.ai)") |
| from models import query_openrouter |
| ai_response = query_openrouter(prompt_for_ai, context, is_code_gen) |
| else: |
| |
| ai_response = query_ai_with_fallback(prompt_for_ai, context, is_code_gen) |
| |
| response_data['response'] = ai_response |
|
|
| |
| |
| |
| if not regenerate or regenerate_from is None: |
| stored_content = display_message if 'display_message' in locals() else full_message |
| ai_ready_content = ai_message if 'ai_message' in locals() else full_message |
| |
| conv['messages'].append({ |
| 'role': 'user', |
| 'content': stored_content, |
| 'ai_content': ai_ready_content, |
| 'timestamp': datetime.now().isoformat(), |
| 'attachments': attached_files if attached_files else None, |
| 'id': generate_message_id() |
| }) |
| conv['branch_root'] = None |
|
|
| |
| if not (regenerate and regenerate_from is not None): |
| conv['messages'].append({ |
| 'role': 'assistant', |
| 'content': response_data['response'], |
| 'timestamp': datetime.now().isoformat(), |
| 'id': generate_message_id() |
| }) |
|
|
| |
| if conv['title'] == 'New Chat': |
| raw_title = generate_chat_title(conv['messages']) |
| conv['title'] = sanitize_filename(raw_title) |
| response_data['title'] = conv['title'] |
|
|
| conv['last_updated'] = datetime.now().isoformat() |
| save_user_conversations_for_current_user(conversations) |
|
|
| return jsonify(response_data) |
| @app.route('/api/edit_message', methods=['POST']) |
| @require_auth |
| def edit_message(): |
| data = request.json |
| conversation_id = data.get('conversation_id') |
| message_index = data.get('message_index') |
| new_content = data.get('new_content') |
| attached_files = data.get('attached_files', []) |
| |
| user_id = get_current_user_id() |
| conversations = get_user_conversations_for_current_user() |
|
|
| if conversation_id not in conversations: |
| return jsonify({'error': 'Conversation not found'}), 404 |
|
|
| conv = conversations[conversation_id] |
|
|
| if 'versions' not in conv: |
| conv['versions'] = {} |
| if 'current_version_index' not in conv: |
| conv['current_version_index'] = {} |
| if 'version_branches' not in conv: |
| conv['version_branches'] = {} |
|
|
| messages = conv['messages'] |
|
|
| if message_index >= len(messages): |
| return jsonify({'error': 'Message not found'}), 404 |
|
|
| |
| if messages[message_index]['role'] != 'user': |
| return jsonify({'error': 'Can only edit user messages'}), 400 |
|
|
| full_content = new_content |
| if attached_files: |
| file_names = [file_info['name'] for file_info in attached_files] |
| file_list = ", ".join(file_names) |
| full_content = new_content + f"\n\n[Attached files: {file_list}]" |
|
|
| |
| if message_index + 1 < len(messages): |
| msg_key = str(message_index + 1) |
| if msg_key not in conv['versions']: |
| conv['versions'][msg_key] = [] |
|
|
| old_response = messages[message_index + 1]['content'] |
| current_version_idx = conv['current_version_index'].get(msg_key, 0) |
| |
| if old_response not in conv['versions'][msg_key]: |
| conv['versions'][msg_key].append(old_response) |
| conv['current_version_index'][msg_key] = len(conv['versions'][msg_key]) - 1 |
| current_version_idx = len(conv['versions'][msg_key]) - 1 |
| |
| |
| subsequent_messages = messages[message_index + 2:] |
| if subsequent_messages: |
| version_branch_key = f"{msg_key}_v{current_version_idx}" |
| conv['version_branches'][version_branch_key] = subsequent_messages |
|
|
| |
| ai_ready_content = new_content |
| if attached_files: |
| file_context = "" |
| for file_info in attached_files: |
| if 'content' in file_info: |
| file_context += file_info['content'] |
| if file_context: |
| ai_ready_content = new_content + file_context |
| |
| |
| messages[message_index]['content'] = full_content |
| messages[message_index]['ai_content'] = ai_ready_content |
| messages[message_index]['attachments'] = attached_files if attached_files else None |
| messages[message_index]['edited'] = True |
| messages[message_index]['edited_at'] = datetime.now().isoformat() |
|
|
| |
| while len(messages) > message_index + 1: |
| messages.pop() |
|
|
| |
| context = [] |
| for msg in messages: |
| if msg['role'] == 'user' and 'ai_content' in msg: |
| context.append({"role": msg['role'], "content": msg['ai_content']}) |
| else: |
| context.append({"role": msg['role'], "content": msg['content']}) |
| |
| print(f"Edit context messages count: {len(context)}") |
|
|
| is_code_gen = is_code_generation_request(full_content) |
| |
| if is_code_gen: |
| if not any(phrase in full_content.lower() for phrase in ['summarize', 'explain', 'what is', 'tell me about']): |
| full_content += "\n\nIMPORTANT: Provide the COMPLETE, FULLY FUNCTIONAL code with at least 500 lines. Do not abbreviate or use placeholders." |
| |
| |
| has_attachments = attached_files and len(attached_files) > 0 |
| if has_attachments: |
| print(f"π Edit: Attachments detected ({len(attached_files)} files) - forcing OpenRouter only (no Pollinations.ai)") |
| from models import query_openrouter |
| ai_response = query_openrouter(full_content, context, is_code_gen) |
| else: |
| |
| ai_response = query_ai_with_fallback(full_content, context, is_code_gen) |
|
|
| messages.append({ |
| 'role': 'assistant', |
| 'content': ai_response, |
| 'timestamp': datetime.now().isoformat(), |
| 'id': generate_message_id(), |
| 'edited_response': True |
| }) |
|
|
| msg_key = str(message_index + 1) |
| if msg_key not in conv['versions']: |
| conv['versions'][msg_key] = [] |
|
|
| if ai_response not in conv['versions'][msg_key]: |
| conv['versions'][msg_key].append(ai_response) |
| conv['current_version_index'][msg_key] = len(conv['versions'][msg_key]) - 1 |
|
|
| if conv['title'] == 'New Chat': |
| conv['title'] = generate_chat_title(messages) |
|
|
| conv['last_updated'] = datetime.now().isoformat() |
| save_user_conversations_for_current_user(conversations) |
|
|
| return jsonify({ |
| 'success': True, |
| 'new_response': ai_response, |
| 'conversation_id': conversation_id, |
| 'messages': messages, |
| 'versions': conv.get('versions', {}), |
| 'current_version_index': conv.get('current_version_index', {}) |
| }) |
|
|
| @app.route('/api/switch_version', methods=['POST']) |
| @require_auth |
| def switch_version(): |
| data = request.json |
| conversation_id = data.get('conversation_id') |
| message_index = data.get('message_index') |
| version_index = data.get('version_index') |
| |
| user_id = get_current_user_id() |
| conversations = get_user_conversations_for_current_user() |
|
|
| if conversation_id not in conversations: |
| return jsonify({'error': 'Conversation not found'}), 404 |
|
|
| conv = conversations[conversation_id] |
| versions = conv.get('versions', {}) |
| msg_key = str(message_index) |
|
|
| if msg_key not in versions or version_index >= len(versions[msg_key]): |
| return jsonify({'error': 'Version not found'}), 404 |
|
|
| if 'current_version_index' not in conv: |
| conv['current_version_index'] = {} |
| |
| previous_version_index = conv['current_version_index'].get(msg_key) |
| conv['current_version_index'][msg_key] = version_index |
|
|
| |
| base_messages = conv['messages'][:message_index] |
| version_response = versions[msg_key][version_index] |
| |
| version_branch_key = f"{msg_key}_v{version_index}" |
| branch_messages = conv.get('version_branches', {}).get(version_branch_key, []) |
| |
| new_messages = list(base_messages) |
| |
| if message_index < len(conv['messages']): |
| assistant_msg = dict(conv['messages'][message_index]) |
| assistant_msg['content'] = version_response |
| new_messages.append(assistant_msg) |
| |
| if branch_messages: |
| new_messages.extend(branch_messages) |
| elif version_index == previous_version_index: |
| subsequent = conv['messages'][message_index + 1:] |
| if 'version_branches' not in conv: |
| conv['version_branches'] = {} |
| conv['version_branches'][version_branch_key] = list(subsequent) |
| new_messages.extend(subsequent) |
| |
| conv['messages'] = new_messages |
| conv['branch_root'] = message_index |
| conv['branch_version'] = version_index |
|
|
| save_user_conversations_for_current_user(conversations) |
|
|
| return jsonify({ |
| 'success': True, |
| 'content': version_response, |
| 'current_version_index': conv['current_version_index'], |
| 'messages': conv['messages'] |
| }) |
|
|
| @app.route('/api/rename_chat', methods=['POST']) |
| @require_auth |
| def rename_chat(): |
| data = request.json |
| conversation_id = data.get('conversation_id') |
| new_title = data.get('new_title') |
| |
| user_id = get_current_user_id() |
| conversations = get_user_conversations_for_current_user() |
|
|
| if conversation_id not in conversations: |
| return jsonify({'error': 'Conversation not found'}), 404 |
|
|
| conversations[conversation_id]['title'] = sanitize_filename(new_title) |
| save_user_conversations_for_current_user(conversations) |
|
|
| return jsonify({'success': True}) |
|
|
| @app.route('/api/export_chat/<conversation_id>', methods=['GET']) |
| @require_auth |
| def export_chat(conversation_id): |
| user_id = get_current_user_id() |
| conversations = get_user_conversations_for_current_user() |
| |
| if conversation_id not in conversations: |
| return jsonify({'error': 'Conversation not found'}), 404 |
|
|
| doc = export_to_word(conversations[conversation_id]) |
|
|
| file_stream = BytesIO() |
| doc.save(file_stream) |
| file_stream.seek(0) |
|
|
| title = conversations[conversation_id]['title'] |
| title = ''.join(char for char in title if char.isprintable() and char not in '\n\r\t') |
| title = title.replace('/', '-').replace('\\', '-').replace(':', '-') |
| |
| filename = f"HenAi_Chat_{title}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx" |
|
|
| return send_file( |
| file_stream, |
| as_attachment=True, |
| download_name=filename, |
| mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document' |
| ) |
|
|
| @app.route('/api/get_version', methods=['POST']) |
| @require_auth |
| def get_version(): |
| data = request.json |
| conversation_id = data.get('conversation_id') |
| message_index = data.get('message_index') |
| version_index = data.get('version_index') |
| |
| user_id = get_current_user_id() |
| conversations = get_user_conversations_for_current_user() |
|
|
| if conversation_id not in conversations: |
| return jsonify({'error': 'Conversation not found'}), 404 |
|
|
| conv = conversations[conversation_id] |
| versions = conv.get('versions', {}) |
| msg_key = str(message_index) |
|
|
| if msg_key not in versions or version_index >= len(versions[msg_key]): |
| return jsonify({'error': 'Version not found'}), 404 |
|
|
| return jsonify({ |
| 'success': True, |
| 'content': versions[msg_key][version_index] |
| }) |
|
|
| @app.route('/api/upload', methods=['POST']) |
| def upload_file(): |
| if 'file' not in request.files: |
| return jsonify({'error': 'No file uploaded'}), 400 |
|
|
| file = request.files['file'] |
|
|
| if file.filename == '': |
| return jsonify({'error': 'No file selected'}), 400 |
|
|
| if not allowed_file(file.filename): |
| return jsonify({'error': 'File type not allowed. Please upload text files, code files, or documents.'}), 400 |
|
|
| filename = secure_filename(file.filename) |
| file_content = file.read() |
|
|
| try: |
| text_content = extract_text_from_file(file_content, filename) |
| |
| file_extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else 'txt' |
| formatted_content = f"\n\n--- FILE: {filename} ({file_extension}) ---\n{text_content}\n--- END FILE: {filename} ---\n\n" |
|
|
| return jsonify({ |
| 'success': True, |
| 'filename': filename, |
| 'content': formatted_content, |
| 'preview': text_content[:500] + ('...' if len(text_content) > 500 else '') |
| }) |
| except Exception as e: |
| return jsonify({'error': f'Error processing file: {str(e)}'}), 500 |
|
|
| @app.route('/api/add_pending_attachment', methods=['POST']) |
| def add_pending_attachment(): |
| data = request.json |
| conversation_id = data.get('conversation_id') |
| filename = data.get('filename') |
| content = data.get('content') |
| |
| if not conversation_id or not filename: |
| return jsonify({'error': 'Missing conversation_id or filename'}), 400 |
| |
| if content: |
| content = content.encode('utf-8', errors='replace').decode('utf-8') |
| |
| if conversation_id not in conversations: |
| return jsonify({'error': 'Conversation not found'}), 404 |
| |
| conv = conversations[conversation_id] |
| |
| if 'pending_attachments' not in conv: |
| conv['pending_attachments'] = [] |
| |
| conv['pending_attachments'].append({ |
| 'name': filename, |
| 'content': content, |
| 'timestamp': datetime.now().isoformat(), |
| 'source': 'archive_search' |
| }) |
| |
| conv['last_updated'] = datetime.now().isoformat() |
| |
| try: |
| save_conversations(conversations) |
| except Exception as e: |
| print(f"Error saving: {e}") |
| return jsonify({'error': f'Failed to save attachment: {str(e)}'}), 500 |
| |
| return jsonify({ |
| 'success': True, |
| 'conversation_id': conversation_id, |
| 'filename': filename, |
| 'attachments_count': len(conv['pending_attachments']) |
| }) |
|
|
| @app.route('/api/attach_to_chat', methods=['POST']) |
| def attach_to_chat(): |
| data = request.json |
| conversation_id = data.get('conversation_id') |
| filename = data.get('filename') |
| content = data.get('content') |
| |
| if not conversation_id or not filename: |
| return jsonify({'error': 'Missing conversation_id or filename'}), 400 |
| |
| if content: |
| content = content.encode('utf-8', errors='replace').decode('utf-8') |
| |
| if conversation_id not in conversations: |
| conversation_id = str(uuid.uuid4()) |
| conversations[conversation_id] = { |
| 'id': conversation_id, |
| 'messages': [], |
| 'title': 'New Chat', |
| 'created_at': datetime.now().isoformat(), |
| 'last_updated': datetime.now().isoformat(), |
| 'versions': {}, |
| 'current_version_index': {}, |
| 'branch_root': None, |
| 'pending_attachments': [] |
| } |
| |
| conv = conversations[conversation_id] |
| |
| if 'pending_attachments' not in conv: |
| conv['pending_attachments'] = [] |
| |
| conv['pending_attachments'].append({ |
| 'name': filename, |
| 'content': content, |
| 'timestamp': datetime.now().isoformat(), |
| 'source': 'archive_search' |
| }) |
| |
| conv['last_updated'] = datetime.now().isoformat() |
| |
| try: |
| save_conversations(conversations) |
| except Exception as e: |
| print(f"Error saving: {e}") |
| if 'pending_attachments' in conv and conv['pending_attachments']: |
| conv['pending_attachments'].pop() |
| save_conversations(conversations) |
| |
| return jsonify({ |
| 'success': True, |
| 'conversation_id': conversation_id, |
| 'filename': filename, |
| 'attachments_count': len(conv['pending_attachments']) |
| }) |
|
|
| @app.route('/api/conversations', methods=['GET']) |
| @require_auth |
| def get_conversations(): |
| conv_list = get_user_conversations_list(get_current_user_id()) |
| return jsonify(conv_list) |
|
|
| @app.route('/api/conversation/<conversation_id>', methods=['GET']) |
| @require_auth |
| def get_conversation(conversation_id): |
| user_id = get_current_user_id() |
| conversations = get_user_conversations_for_current_user() |
| |
| if conversation_id in conversations: |
| conv_data = conversations[conversation_id] |
| |
| messages = conv_data.get('messages', []) |
| for msg in messages: |
| if msg.get('media_data'): |
| msg['media_data'] = msg['media_data'] |
| |
| return jsonify({ |
| **conv_data, |
| 'versions': conv_data.get('versions', {}), |
| 'current_version_index': conv_data.get('current_version_index', {}), |
| 'version_branches': conv_data.get('version_branches', {}), |
| 'branch_root': conv_data.get('branch_root'), |
| 'branch_version': conv_data.get('branch_version'), |
| 'pending_attachments': conv_data.get('pending_attachments', []) |
| }) |
| return jsonify({'error': 'Conversation not found'}), 404 |
|
|
| @app.route('/api/delete_conversation/<conversation_id>', methods=['DELETE']) |
| @require_auth |
| def delete_conversation(conversation_id): |
| user_id = get_current_user_id() |
| conversations = get_user_conversations_for_current_user() |
| |
| if conversation_id in conversations: |
| del conversations[conversation_id] |
| save_user_conversations_for_current_user(conversations) |
| return jsonify({'success': True}) |
| return jsonify({'error': 'Conversation not found'}), 404 |
|
|
| @app.route('/api/remove_pending_attachment', methods=['POST']) |
| def remove_pending_attachment(): |
| data = request.json |
| conversation_id = data.get('conversation_id') |
| attachment_index = data.get('attachment_index') |
| |
| if conversation_id not in conversations: |
| return jsonify({'error': 'Conversation not found'}), 404 |
| |
| conv = conversations[conversation_id] |
| |
| if 'pending_attachments' not in conv: |
| return jsonify({'error': 'No pending attachments'}), 404 |
| |
| if attachment_index >= len(conv['pending_attachments']): |
| return jsonify({'error': 'Attachment not found'}), 404 |
| |
| conv['pending_attachments'].pop(attachment_index) |
| |
| if len(conv['pending_attachments']) == 0: |
| conv['pending_attachments'] = [] |
| |
| conv['last_updated'] = datetime.now().isoformat() |
| save_conversations(conversations) |
| |
| return jsonify({'success': True, 'remaining': len(conv['pending_attachments'])}) |
|
|
| @app.route('/api/clear_pending_attachments', methods=['POST']) |
| def clear_pending_attachments(): |
| data = request.json |
| conversation_id = data.get('conversation_id') |
| |
| if conversation_id not in conversations: |
| return jsonify({'error': 'Conversation not found'}), 404 |
| |
| conv = conversations[conversation_id] |
| conv['pending_attachments'] = [] |
| conv['last_updated'] = datetime.now().isoformat() |
| save_conversations(conversations) |
| |
| return jsonify({'success': True}) |
|
|
| |
|
|
| from workspace import load_workspace, save_workspace, build_folder_context, get_file_content_from_workspace |
|
|
| @app.route('/api/ide/chat', methods=['POST']) |
| def ide_chat(): |
| data = request.json |
| message = data.get('message', '') |
| folder_path = data.get('folder_path', '') |
| selected_file = data.get('selected_file', '') |
| conversation_history = data.get('conversation_history', []) |
| include_full_folder = data.get('include_full_folder', True) |
| |
| if not message: |
| return jsonify({'error': 'No message provided'}), 400 |
| |
| context_content = "" |
| |
| if include_full_folder and folder_path: |
| workspace = load_workspace() |
| parts = folder_path.split('/') if folder_path else [] |
| current = workspace |
| for part in parts: |
| found = False |
| for folder in current.get('folders', []): |
| if folder.get('name') == part: |
| current = folder |
| found = True |
| break |
| if not found: |
| break |
| |
| context_content = build_folder_context(current, folder_path) |
| |
| if selected_file: |
| file_content = get_file_content_from_workspace(selected_file) |
| if file_content: |
| context_content += f"\n\n--- SELECTED FILE: {selected_file} ---\n" |
| context_content += file_content |
| context_content += f"\n--- END FILE: {selected_file} ---\n" |
| |
| system_prompt = """You are HenAi, an expert coding assistant integrated into the HenIde workspace. |
| You have access to the entire folder structure and file contents when the checkbox is enabled. |
| |
| IMPORTANT RULES: |
| 1. When providing code, output ONLY the code with no explanations before or after |
| 2. Use proper code blocks with language specification |
| 3. Understand the context of the entire folder structure |
| 4. When asked to debug, explain what's wrong and provide the corrected code |
| 5. When asked to generate new files, specify the filename and provide the complete code |
| 6. Keep responses concise but thorough |
| |
| Remember: Code output should be pure code with no additional text outside code blocks.""" |
| |
| messages = [{"role": "system", "content": system_prompt}] |
| |
| for msg in conversation_history: |
| messages.append({"role": msg.get('role'), "content": msg.get('content')}) |
| |
| if context_content: |
| context_message = f"FOLDER CONTEXT:\n{context_content}\n\nUSER QUERY: {message}" |
| messages.append({"role": "user", "content": context_message}) |
| else: |
| messages.append({"role": "user", "content": message}) |
| |
| is_code_gen = is_code_generation_request(message) |
| |
| ai_response = query_ai_with_fallback( |
| messages[-1]['content'] if not context_content else context_message, |
| messages[:-1], |
| is_code_gen |
| ) |
| |
| return jsonify({ |
| 'success': True, |
| 'response': ai_response |
| }) |
|
|
| @app.route('/api/ide/create_file', methods=['POST']) |
| def ide_create_file(): |
| data = request.json |
| folder_path = data.get('folder_path', '') |
| file_name = data.get('file_name', '').strip() |
| content = data.get('content', '') |
| |
| if not file_name: |
| return jsonify({'error': 'File name required'}), 400 |
| |
| if not re.match(r'^[a-zA-Z0-9_\-\.]+$', file_name): |
| return jsonify({'error': 'Invalid file name. Use only letters, numbers, underscore, hyphen, and dot.'}), 400 |
| |
| workspace = load_workspace() |
| |
| if folder_path: |
| parts = folder_path.split('/') if folder_path else [] |
| current = workspace |
| for part in parts: |
| found = False |
| for folder in current.get('folders', []): |
| if folder.get('name') == part: |
| current = folder |
| found = True |
| break |
| if not found: |
| return jsonify({'error': f'Folder "{folder_path}" not found'}), 404 |
| target_folder = current |
| else: |
| target_folder = workspace |
| |
| if 'files' not in target_folder: |
| target_folder['files'] = [] |
| |
| for file_item in target_folder['files']: |
| if file_item.get('name') == file_name: |
| return jsonify({'error': 'File already exists'}), 400 |
| |
| new_file = { |
| 'name': file_name, |
| 'content': content, |
| 'created_at': datetime.now().isoformat(), |
| 'updated_at': datetime.now().isoformat() |
| } |
| target_folder['files'].insert(0, new_file) |
| |
| save_workspace(workspace) |
| |
| full_path = folder_path + '/' + file_name if folder_path else file_name |
| print(f"β
IDE: File created successfully - {full_path}") |
| |
| return jsonify({ |
| 'success': True, |
| 'file': new_file, |
| 'full_path': full_path |
| }) |
|
|
| @app.route('/api/ide/create_folder', methods=['POST']) |
| def ide_create_folder(): |
| data = request.json |
| parent_path = data.get('parent_path', '') |
| folder_name = data.get('folder_name', '').strip() |
| |
| if not folder_name: |
| return jsonify({'error': 'Folder name required'}), 400 |
| |
| if not re.match(r'^[a-zA-Z0-9_\-]+$', folder_name): |
| return jsonify({'error': 'Invalid folder name. Use only letters, numbers, underscore, and hyphen.'}), 400 |
| |
| workspace = load_workspace() |
| |
| if parent_path: |
| parts = parent_path.split('/') if parent_path else [] |
| current = workspace |
| for part in parts: |
| found = False |
| for folder in current.get('folders', []): |
| if folder.get('name') == part: |
| current = folder |
| found = True |
| break |
| if not found: |
| return jsonify({'error': f'Parent folder "{parent_path}" not found'}), 404 |
| parent = current |
| else: |
| parent = workspace |
| |
| if 'folders' not in parent: |
| parent['folders'] = [] |
| |
| for folder in parent['folders']: |
| if folder.get('name') == folder_name: |
| return jsonify({'error': 'Folder already exists'}), 400 |
| |
| new_folder = { |
| 'name': folder_name, |
| 'created_at': datetime.now().isoformat(), |
| 'folders': [], |
| 'files': [] |
| } |
| parent['folders'].insert(0, new_folder) |
| |
| save_workspace(workspace) |
| |
| new_path = parent_path + '/' + folder_name if parent_path else folder_name |
| print(f"β
IDE: Folder created successfully - {new_path}") |
| |
| return jsonify({ |
| 'success': True, |
| 'folder': new_folder, |
| 'full_path': new_path |
| }) |
|
|
| @app.route('/api/ide/update_file', methods=['POST']) |
| def ide_update_file(): |
| data = request.json |
| file_path = data.get('file_path', '') |
| content = data.get('content', '') |
| |
| if not file_path: |
| return jsonify({'error': 'File path required'}), 400 |
| |
| workspace = load_workspace() |
| |
| parts = file_path.split('/') |
| file_name = parts[-1] |
| folder_parts = parts[:-1] if len(parts) > 1 else [] |
| |
| current = workspace |
| for folder_name in folder_parts: |
| found = False |
| for folder in current.get('folders', []): |
| if folder.get('name') == folder_name: |
| current = folder |
| found = True |
| break |
| if not found: |
| return jsonify({'error': f'Folder "{folder_name}" not found in path'}), 404 |
| |
| files = current.get('files', []) |
| for i, file_item in enumerate(files): |
| if file_item.get('name') == file_name: |
| file_item['content'] = content |
| file_item['updated_at'] = datetime.now().isoformat() |
| files[i] = file_item |
| save_workspace(workspace) |
| print(f"β
IDE: File saved successfully - {file_path}") |
| return jsonify({'success': True, 'file': file_item}) |
| |
| return jsonify({'error': f'File "{file_name}" not found in folder'}), 404 |
|
|
| @app.route('/api/ide/delete_file', methods=['POST']) |
| def ide_delete_file(): |
| data = request.json |
| file_path = data.get('file_path', '') |
| |
| if not file_path: |
| return jsonify({'error': 'File path required'}), 400 |
| |
| workspace = load_workspace() |
| |
| parts = file_path.split('/') |
| file_name = parts[-1] |
| folder_parts = parts[:-1] if len(parts) > 1 else [] |
| |
| current = workspace |
| for folder_name in folder_parts: |
| found = False |
| for folder in current.get('folders', []): |
| if folder.get('name') == folder_name: |
| current = folder |
| found = True |
| break |
| if not found: |
| return jsonify({'error': f'Folder not found'}), 404 |
| |
| files = current.get('files', []) |
| for i, file_item in enumerate(files): |
| if file_item.get('name') == file_name: |
| del files[i] |
| save_workspace(workspace) |
| return jsonify({'success': True}) |
| |
| return jsonify({'error': 'File not found'}), 404 |
|
|
| @app.route('/api/user/profile', methods=['GET']) |
| @require_auth |
| def get_user_profile(): |
| """Get current user's profile information""" |
| user_id = get_current_user_id() |
| if not user_id: |
| return jsonify({'success': False, 'error': 'Not authenticated'}), 401 |
| |
| profile = load_user_profile(user_id) |
| |
| return jsonify({ |
| 'success': True, |
| 'profile': { |
| 'username': profile.get('username', 'User'), |
| 'fullname': profile.get('fullname', profile.get('username', 'User')), |
| 'email': profile.get('email', ''), |
| 'avatar': profile.get('avatar', None), |
| 'theme': profile.get('theme', 'dark'), |
| 'user_id': user_id |
| } |
| }) |
|
|
| @app.route('/api/user/profile', methods=['PUT']) |
| @require_auth |
| def update_user_profile(): |
| """Update current user's profile information""" |
| user_id = get_current_user_id() |
| if not user_id: |
| return jsonify({'success': False, 'error': 'Not authenticated'}), 401 |
| |
| data = request.json |
| profile = load_user_profile(user_id) |
| |
| if 'username' in data: |
| profile['username'] = data['username'] |
| if 'fullname' in data: |
| profile['fullname'] = data['fullname'] |
| if 'avatar' in data: |
| profile['avatar'] = data['avatar'] |
| |
| if save_user_profile(user_id, profile): |
| return jsonify({'success': True, 'profile': profile}) |
| else: |
| return jsonify({'success': False, 'error': 'Failed to save profile'}), 500 |
|
|
| |
|
|
| from media import media_handler |
|
|
| @app.route('/api/media/search/image', methods=['POST']) |
| def search_image(): |
| data = request.json |
| query = data.get('query', '').strip() |
| provider = data.get('provider') |
| |
| if not query: |
| return jsonify({'error': 'No query provided'}), 400 |
| |
| if provider: |
| result = media_handler.search_images(query, provider) |
| else: |
| result = media_handler.search_with_fallback(query, 'image') |
| |
| return jsonify(result) |
|
|
| @app.route('/api/media/search/video', methods=['POST']) |
| def search_video(): |
| data = request.json |
| query = data.get('query', '').strip() |
| provider = data.get('provider') |
| |
| if not query: |
| return jsonify({'error': 'No query provided'}), 400 |
| |
| if provider: |
| result = media_handler.search_videos(query, provider) |
| else: |
| result = media_handler.search_with_fallback(query, 'video') |
| |
| return jsonify(result) |
|
|
| @app.route('/api/media/regenerate', methods=['POST']) |
| def regenerate_media(): |
| data = request.json |
| query = data.get('query', '').strip() |
| media_type = data.get('media_type', 'image') |
| current_id = data.get('current_id') |
| provider = data.get('provider') |
| |
| if not query: |
| return jsonify({'error': 'No query provided'}), 400 |
| |
| result = media_handler.regenerate_with_fallback(query, media_type, current_id, provider) |
| |
| return jsonify(result) |
|
|
| @app.route('/api/media/analyze/video', methods=['POST']) |
| def analyze_video(): |
| data = request.json |
| video_url = data.get('video_url', '') |
| video_name = data.get('video_name', 'video.mp4') |
| |
| if not video_url: |
| return jsonify({'error': 'No video URL provided'}), 400 |
| |
| result = media_handler.analyze_video(video_url, video_name) |
| return jsonify(result) |
|
|
| @app.route('/api/media/analyze/image', methods=['POST']) |
| def analyze_image(): |
| """Analyze image from URL or uploaded file using OCR first, then Vision, then AI""" |
| image_content = None |
| image_name = None |
| image_url = None |
| |
| |
| |
| |
| |
| if request.files and 'image_file' in request.files: |
| |
| file = request.files['image_file'] |
| if file.filename == '': |
| return jsonify({'error': 'No file selected'}), 400 |
| |
| image_name = secure_filename(file.filename) |
| image_content = file.read() |
| image_url = None |
| print(f"π Received uploaded image: {image_name} ({len(image_content)} bytes)") |
| |
| else: |
| |
| data = request.json or {} |
| image_url = data.get('image_url', '') |
| image_name = data.get('image_name', 'image.jpg') |
| |
| if not image_url: |
| return jsonify({'error': 'No image URL or file provided'}), 400 |
| |
| |
| try: |
| print(f"π₯ Downloading image from URL: {image_url}") |
| response = requests.get(image_url, timeout=30, stream=True) |
| response.raise_for_status() |
| image_content = response.content |
| print(f"β
Downloaded {len(image_content)} bytes") |
| except requests.exceptions.RequestException as e: |
| print(f"β Failed to download image: {e}") |
| return jsonify({ |
| 'success': False, |
| 'error': f'Failed to download image: {str(e)}' |
| }), 500 |
| |
| if not image_content: |
| return jsonify({'success': False, 'error': 'No image content to analyze'}), 400 |
| |
| temp_file_path = None |
| try: |
| import tempfile |
| import re |
| |
| ext = image_name.split('.')[-1].lower() if '.' in image_name else 'jpg' |
| if ext not in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']: |
| ext = 'jpg' |
| |
| with tempfile.NamedTemporaryFile(suffix=f'.{ext}', delete=False) as tmp: |
| tmp.write(image_content) |
| tmp.flush() |
| temp_file_path = tmp.name |
| |
| print(f"π Image saved to temp file: {temp_file_path}") |
| |
| |
| ocr_text = "" |
| try: |
| print("π Running EasyOCR for text extraction...") |
| import easyocr |
| reader = easyocr.Reader(['en'], gpu=False) |
| result = reader.readtext(image_content) |
| if result: |
| ocr_text = ' '.join([item[1] for item in result]) |
| print(f"β
EasyOCR extracted {len(ocr_text)} characters") |
| except ImportError: |
| print("β οΈ EasyOCR not installed, trying fallback OCR...") |
| try: |
| |
| processed_output = binary_processor.process_image(image_content, image_name) |
| if "EXTRACTED TEXT:" in processed_output: |
| ocr_match = re.search(r'EXTRACTED TEXT:\n(.*?)(?=\n--- END IMAGE ANALYSIS|--- END FILE)', processed_output, re.DOTALL) |
| if ocr_match: |
| ocr_text = ocr_match.group(1).strip() |
| print(f"β
Fallback OCR extracted {len(ocr_text)} characters") |
| elif "No text detected" not in processed_output: |
| lines = processed_output.split('\n') |
| for i, line in enumerate(lines): |
| if 'π EXTRACTED TEXT:' in line and i + 1 < len(lines): |
| ocr_text = lines[i + 1].strip() |
| print(f"β
Fallback OCR extracted {len(ocr_text)} characters") |
| break |
| except Exception as e: |
| print(f"β οΈ Fallback OCR failed: {e}") |
| except Exception as e: |
| print(f"β οΈ EasyOCR error: {e}") |
| |
| |
| vision_analysis = "" |
| try: |
| from vision import get_vision_model |
| vision_model = get_vision_model() |
| print("π Analyzing image content with Vision Model...") |
| vision_analysis = vision_model.analyze_image(image_content) |
| if vision_analysis: |
| print(f"β
Vision Model analysis ({len(vision_analysis)} chars)") |
| except Exception as e: |
| print(f"β οΈ Vision Model error: {e}") |
| |
| |
| clean_analysis = "" |
| |
| |
| ai_prompt_parts = [] |
| |
| |
| try: |
| from PIL import Image |
| import io |
| pil_img = Image.open(io.BytesIO(image_content)) |
| width, height = pil_img.size |
| img_info = f"Image dimensions: {width} by {height} pixels" |
| ai_prompt_parts.append(img_info) |
| except: |
| pass |
| |
| if ocr_text and len(ocr_text.strip()) > 10: |
| |
| cleaned_ocr = re.sub(r'\s+', ' ', ocr_text[:2000]).strip() |
| ai_prompt_parts.append(f"TEXT FROM IMAGE:\n{cleaned_ocr}") |
| |
| if vision_analysis and len(vision_analysis) > 10: |
| ai_prompt_parts.append(f"VISUAL DESCRIPTION:\n{vision_analysis}") |
| |
| if not ai_prompt_parts: |
| |
| name_without_ext = re.sub(r'\.[^.]+$', '', image_name) |
| clean_name = re.sub(r'[_\-\.]', ' ', name_without_ext) |
| clean_name = re.sub(r'\d+', '', clean_name).strip() |
| if clean_name and len(clean_name) > 3: |
| clean_analysis = f"This image appears to be related to {clean_name}. However, no detailed text or visual description could be extracted from the image." |
| else: |
| clean_analysis = "The image has been processed, but no readable text or recognizable content was detected. This could be due to the image quality, low contrast, or lack of clear visual elements." |
| print("β οΈ Using filename fallback") |
| else: |
| |
| combined_context = "\n\n".join(ai_prompt_parts) |
| |
| ai_analysis_prompt = f"""You are an expert image analyst. Based on the information below, write a clear, well-structured analysis. |
| |
| INFORMATION FROM IMAGE: |
| {combined_context} |
| |
| CRITICAL RULES: |
| 1. NEVER use phrases like "The image contains text that reads" or "OCR text found" or "The image also contains" |
| 2. NEVER just list the extracted text - instead, read it and describe what it means |
| 3. Write in complete sentences with proper punctuation |
| 4. Break long text into 2-3 short paragraphs |
| 5. Each paragraph should be 2-4 sentences |
| 6. Be descriptive but concise |
| |
| EXAMPLE OF GOOD OUTPUT (for a government services portal): |
| "The image shows a government services dashboard with multiple service categories. These include licensing, permits, registration, and payments. A driving license renewal option is available for 650 Kenyan Shillings, with a processing time of 3-5 business days." |
| |
| EXAMPLE OF GOOD OUTPUT (for a case study question): |
| "The image displays a case study discussion question about TrendSwift Fashion, an online clothing retailer. The company ships to 30 countries but faces challenges with disconnected systems. They use separate platforms for their website, warehouse robots, and delivery partners." |
| |
| Now write your analysis following the GOOD EXAMPLE format. Keep it concise and well-organized:""" |
| |
| try: |
| from models import query_openrouter |
| print("π€ Sending to AI for comprehensive analysis...") |
| ai_response = query_openrouter(ai_analysis_prompt, context=None, is_code_generation=False) |
| if ai_response and len(ai_response) > 50: |
| |
| clean_analysis = ai_response |
| |
| |
| clean_analysis = re.sub(r'The image contains text that reads:?\s*', '', clean_analysis, flags=re.IGNORECASE) |
| clean_analysis = re.sub(r'Also contains text:?\s*', '', clean_analysis, flags=re.IGNORECASE) |
| clean_analysis = re.sub(r'OCR text found:?\s*', '', clean_analysis, flags=re.IGNORECASE) |
| clean_analysis = re.sub(r'Text extracted from the image:?\s*', '', clean_analysis, flags=re.IGNORECASE) |
| clean_analysis = re.sub(r'TEXT FROM IMAGE:?\s*', '', clean_analysis, flags=re.IGNORECASE) |
| clean_analysis = re.sub(r'VISUAL DESCRIPTION:?\s*', '', clean_analysis, flags=re.IGNORECASE) |
| |
| |
| sentences = re.split(r'(?<=[.!?])\s+(?=[A-Z])', clean_analysis) |
| if len(sentences) > 1: |
| clean_analysis = ' '.join(sentences) |
| |
| |
| if clean_analysis and len(clean_analysis) > 0: |
| clean_analysis = clean_analysis[0].upper() + clean_analysis[1:] if len(clean_analysis) > 1 else clean_analysis.upper() |
| |
| |
| clean_analysis = re.sub(r'\bThe image also contains text\b.*?\.', '', clean_analysis, flags=re.IGNORECASE) |
| clean_analysis = re.sub(r'\bAlso contains text\b.*?\.', '', clean_analysis, flags=re.IGNORECASE) |
| |
| |
| clean_analysis = re.sub(r'\s+', ' ', clean_analysis) |
| clean_analysis = re.sub(r'\n\s*\n', '\n\n', clean_analysis) |
| |
| |
| if len(clean_analysis) < 100 or 'contains text' in clean_analysis.lower(): |
| if ocr_text and len(ocr_text) > 50: |
| ocr_lines = ocr_text.split('.') |
| meaningful_lines = [line.strip() for line in ocr_lines if len(line.strip()) > 20] |
| if meaningful_lines: |
| clean_analysis = '. '.join(meaningful_lines[:3]) + '.' |
| if len(clean_analysis) < 50: |
| clean_analysis = f"This image shows: {ocr_text[:200]}" |
| |
| print(f"β
AI analysis complete ({len(clean_analysis)} chars)") |
| else: |
| |
| if vision_analysis and len(vision_analysis) > 20: |
| clean_analysis = vision_analysis |
| if ocr_text and len(ocr_text.strip()) > 10: |
| ocr_clean = re.sub(r'\s+', ' ', ocr_text[:300]).strip() |
| clean_analysis += f" {ocr_clean}" |
| elif ocr_text and len(ocr_text.strip()) > 10: |
| ocr_clean = re.sub(r'\s+', ' ', ocr_text[:500]).strip() |
| clean_analysis = ocr_clean |
| elif vision_analysis: |
| clean_analysis = vision_analysis |
| else: |
| clean_analysis = "The image has been analyzed, but no specific content could be identified. Please try a clearer image or different source." |
| except Exception as e: |
| print(f"β οΈ AI analysis failed: {e}, using fallback") |
| if vision_analysis and len(vision_analysis) > 20: |
| clean_analysis = vision_analysis |
| if ocr_text and len(ocr_text.strip()) > 10: |
| ocr_clean = re.sub(r'\s+', ' ', ocr_text[:300]).strip() |
| clean_analysis += f" {ocr_clean}" |
| elif ocr_text and len(ocr_text.strip()) > 10: |
| ocr_clean = re.sub(r'\s+', ' ', ocr_text[:500]).strip() |
| clean_analysis = ocr_clean |
| else: |
| clean_analysis = "The image has been analyzed, but no specific content could be identified. Please try a clearer image or different source." |
| |
| |
| if clean_analysis: |
| |
| if '\n\n' not in clean_analysis and len(clean_analysis) > 300: |
| sentences = re.split(r'(?<=[.!?])\s+(?=[A-Z])', clean_analysis) |
| if len(sentences) > 3: |
| paragraphs = [] |
| for i in range(0, len(sentences), 3): |
| para = ' '.join(sentences[i:i+3]) |
| paragraphs.append(para) |
| clean_analysis = '\n\n'.join(paragraphs) |
| |
| |
| clean_analysis = re.sub(r'\. ([a-z])', lambda m: '. ' + m.group(1).upper(), clean_analysis) |
| clean_analysis = re.sub(r'\! ([a-z])', lambda m: '! ' + m.group(1).upper(), clean_analysis) |
| clean_analysis = re.sub(r'\? ([a-z])', lambda m: '? ' + m.group(1).upper(), clean_analysis) |
| |
| |
| if clean_analysis and len(clean_analysis) > 0: |
| clean_analysis = clean_analysis[0].upper() + clean_analysis[1:] if len(clean_analysis) > 1 else clean_analysis.upper() |
| |
| if not clean_analysis or len(clean_analysis) < 10: |
| |
| if ocr_text and len(ocr_text) > 20: |
| ocr_clean = re.sub(r'\s+', ' ', ocr_text[:300]).strip() |
| sentences = re.split(r'[.!?]+', ocr_clean) |
| meaningful = [s.strip() for s in sentences if len(s.strip()) > 15] |
| if meaningful: |
| clean_analysis = '. '.join(meaningful[:3]) + '.' |
| else: |
| clean_analysis = ocr_clean[:200] |
| elif vision_analysis and len(vision_analysis) > 20: |
| clean_analysis = vision_analysis |
| else: |
| clean_analysis = "The image has been analyzed, but no specific content could be identified. Please try a clearer image or different source." |
| |
| |
| metadata_patterns = [ |
| r'Photographer:?\s*\S+', |
| r'Photo by\s+\S+', |
| r'Credit:?\s*\S+', |
| r'Source:?\s*\S+', |
| r'Copyright\s+[Β©]?\s*\S+', |
| r'Β©\s*\d{4}\s*\S+', |
| r'\[.*?\]', |
| r'\(.*?(credit|courtesy|source).*?\)', |
| r'Image courtesy of\s+\S+', |
| r'Sourced from\s+\S+', |
| r'^\w+:\s*$', |
| r'\*\*Image Analysis:.*?\*\*', |
| r'\*\*AI Analysis:\*\*', |
| r'\*\*OCR Extracted Text Found:\*\*', |
| r'\*\*Note:\*\*', |
| r'\*\*Image Details:\*\*', |
| r'\*\*Final Analysis:\*\*', |
| r'---.*?---', |
| ] |
| |
| for pattern in metadata_patterns: |
| clean_analysis = re.sub(pattern, '', clean_analysis, flags=re.IGNORECASE | re.MULTILINE) |
| |
| clean_analysis = re.sub(r'\*\*([^*]+)\*\*', r'\1', clean_analysis) |
| clean_analysis = re.sub(r'`([^`]+)`', r'\1', clean_analysis) |
| clean_analysis = re.sub(r'#+\s*', '', clean_analysis) |
| clean_analysis = re.sub(r'\s+', ' ', clean_analysis) |
| clean_analysis = re.sub(r'\n{3,}', '\n\n', clean_analysis) |
| |
| if clean_analysis and len(clean_analysis) > 0: |
| clean_analysis = clean_analysis[0].upper() + clean_analysis[1:] if len(clean_analysis) > 1 else clean_analysis.upper() |
| |
| clean_analysis = re.sub(r'\s*[|;:]\s*$', '', clean_analysis) |
| clean_analysis = clean_analysis.strip() |
| |
| if not clean_analysis or len(clean_analysis) < 5: |
| clean_analysis = "The image has been analyzed, but no specific content could be identified." |
| |
| |
| if not clean_analysis: |
| clean_analysis = "The image has been analyzed, but no specific content could be identified." |
| if not isinstance(clean_analysis, str): |
| clean_analysis = str(clean_analysis) |
| |
| |
| import base64 |
| image_base64 = base64.b64encode(image_content).decode('utf-8') |
| image_data_url = f"data:image/{ext};base64,{image_base64}" |
| |
| response_data = { |
| 'success': True, |
| 'image_url': image_url if image_url else '', |
| 'image_name': image_name if image_name else 'image', |
| 'analysis': clean_analysis[:2000], |
| 'image_data': image_data_url if image_data_url else '', |
| 'is_upload': image_url is None |
| } |
| |
| print(f"β
Analysis complete: {clean_analysis[:100]}...") |
| return jsonify(response_data) |
| |
| except Exception as e: |
| print(f"β Error analyzing image: {e}") |
| import traceback |
| traceback.print_exc() |
| error_message = str(e) |
| if len(error_message) > 200: |
| error_message = error_message[:200] + "..." |
| return jsonify({ |
| 'success': False, |
| 'error': f'Error analyzing image: {error_message}', |
| 'analysis': "The image could not be analyzed due to an error. Please try again with a different image." |
| }), 500 |
| finally: |
| |
| import platform |
| if platform.system() != 'Windows': |
| try: |
| signal.alarm(0) |
| except: |
| pass |
| if temp_file_path: |
| try: |
| import os |
| os.unlink(temp_file_path) |
| except: |
| pass |
|
|
| @app.route('/api/media/download', methods=['POST']) |
| def download_media(): |
| """Download media directly through the app""" |
| data = request.json |
| url = data.get('url', '') |
| media_type = data.get('media_type', 'image') |
| |
| if not url: |
| return jsonify({'error': 'No URL provided'}), 400 |
| |
| result = media_handler.download_media_direct(url, media_type) |
| |
| if result.get('success'): |
| from flask import send_file |
| import io |
| return send_file( |
| io.BytesIO(result['content']), |
| mimetype=result.get('content_type', 'application/octet-stream'), |
| as_attachment=True, |
| download_name=url.split('/')[-1] or (media_type + '.file') |
| ) |
| else: |
| return jsonify({'error': result.get('error', 'Download failed')}), 500 |
|
|
| @app.route('/api/save_media_message', methods=['POST']) |
| def save_media_message(): |
| data = request.json |
| conversation_id = data.get('conversation_id') |
| user_message = data.get('user_message', '') |
| assistant_response = data.get('assistant_response', '') |
| media_data = data.get('media_data') |
| is_error = data.get('is_error', False) |
| |
| if not conversation_id or conversation_id not in conversations: |
| conversation_id = str(uuid.uuid4()) |
| conversations[conversation_id] = { |
| 'id': conversation_id, |
| 'messages': [], |
| 'title': 'New Chat', |
| 'created_at': datetime.now().isoformat(), |
| 'last_updated': datetime.now().isoformat(), |
| 'versions': {}, |
| 'current_version_index': {}, |
| 'branch_root': None, |
| 'pending_attachments': [] |
| } |
| |
| conv = conversations[conversation_id] |
| |
| user_msg = { |
| 'role': 'user', |
| 'content': user_message, |
| 'timestamp': datetime.now().isoformat(), |
| 'attachments': None |
| } |
| conv['messages'].append(user_msg) |
| |
| assistant_msg = { |
| 'role': 'assistant', |
| 'content': assistant_response, |
| 'timestamp': datetime.now().isoformat(), |
| 'media_data': media_data, |
| 'is_media_result': True, |
| 'is_error': is_error |
| } |
| conv['messages'].append(assistant_msg) |
| |
| if conv['title'] == 'New Chat' and user_message: |
| raw_title = generate_chat_title(conv['messages']) |
| conv['title'] = sanitize_filename(raw_title) |
| |
| conv['last_updated'] = datetime.now().isoformat() |
| save_conversations(conversations) |
| |
| return jsonify({ |
| 'success': True, |
| 'conversation_id': conversation_id, |
| 'title': conv['title'] |
| }) |
|
|
| |
|
|
| doc_processor = DocumentProcessor() |
| doc_creator = DocumentCreator() |
|
|
| @app.route('/api/docs/generate', methods=['POST']) |
| def docs_generate(): |
| data = request.json |
| prompt = data.get('prompt', '') |
| doc_type = data.get('doc_type', 'word') |
| template_id = data.get('template_id') |
| |
| if not prompt: |
| return jsonify({'error': 'No prompt provided'}), 400 |
| |
| |
| doc_type_names = { |
| 'word': 'Word Document', |
| 'pdf': 'PDF Document', |
| 'ppt': 'PowerPoint Presentation', |
| 'excel': 'Excel Spreadsheet', |
| 'csv': 'CSV File', |
| 'txt': 'Text File', |
| 'image': 'Image Description' |
| } |
| |
| doc_type_name = doc_type_names.get(doc_type, 'Document') |
| |
| |
| system_prompt = f"""You are an expert document creator. Generate a complete, well-formatted {doc_type_name} based on the user's request. |
| |
| CRITICAL FORMATTING RULES: |
| 1. Use # for main titles (H1) |
| 2. Use ## for section headings (H2) |
| 3. Use ### for subsections (H3) |
| 4. Use - or * for bullet points |
| 5. Use numbers (1., 2., 3.) for numbered lists |
| 6. For tables, use markdown table format with | and - |
| 7. For emphasis, use **bold** and *italic* |
| 8. DO NOT include any markdown code block markers (```) around the entire document |
| 9. DO NOT include phrases like "Here is your document" or "I've created" |
| 10. Output ONLY the document content, no explanations before or after |
| 11. Ensure the document is complete, professional, and well-structured |
| 12. Use proper line breaks between sections |
| |
| Generate a complete, ready-to-use {doc_type_name} now.""" |
|
|
| |
| template_instructions = "" |
| if template_id: |
| template_instructions = f"\n\nUse the following template structure: {template_id} template format." |
| |
| enhanced_prompt = f"{system_prompt}\n\nUSER REQUEST: {prompt}{template_instructions}\n\nGenerate the document now:" |
| |
| |
| max_retries = 3 |
| last_error = None |
| |
| for attempt in range(max_retries): |
| try: |
| from models import query_openrouter |
| |
| |
| context = [{"role": "system", "content": system_prompt}] |
| |
| print(f"π Document generation attempt {attempt + 1}/{max_retries} for {doc_type} document...") |
| response = query_openrouter(enhanced_prompt, context=context, is_code_generation=False) |
| |
| if response and len(response) > 50: |
| |
| import re |
| |
| cleaned = re.sub(r'^```[\w]*\n?', '', response) |
| cleaned = re.sub(r'\n?```$', '', cleaned) |
| |
| cleaned = re.sub(r'^(Here is|I\'ve created|Below is|The following is).*?\n', '', cleaned, flags=re.IGNORECASE) |
| cleaned = cleaned.strip() |
| |
| if len(cleaned) < 50: |
| |
| cleaned = response |
| cleaned = re.sub(r'^(Here is|I\'ve created|Below is).*?\n', '', cleaned, flags=re.IGNORECASE) |
| cleaned = cleaned.strip() |
| |
| if len(cleaned) > 20: |
| print(f"β
Document generation successful on attempt {attempt + 1}") |
| return jsonify({ |
| 'success': True, |
| 'content': cleaned |
| }) |
| else: |
| print(f"β οΈ Attempt {attempt + 1} returned short/empty response") |
| last_error = "Empty response from AI" |
| |
| except Exception as e: |
| print(f"β οΈ Attempt {attempt + 1} failed: {e}") |
| last_error = str(e) |
| if attempt < max_retries - 1: |
| import time |
| time.sleep(2) |
| continue |
| |
| |
| print("OpenRouter failed all retries, trying Pollinations.ai fallback...") |
| for attempt in range(max_retries): |
| try: |
| from models import call_pollinations_ai |
| messages = [{"role": "user", "content": enhanced_prompt}] |
| pollinations_response = call_pollinations_ai(messages) |
| if pollinations_response and len(pollinations_response) > 50: |
| |
| import re |
| cleaned = re.sub(r'^```[\w]*\n?', '', pollinations_response) |
| cleaned = re.sub(r'\n?```$', '', cleaned) |
| cleaned = re.sub(r'^(Here is|I\'ve created|Below is).*?\n', '', cleaned, flags=re.IGNORECASE) |
| cleaned = cleaned.strip() |
| |
| return jsonify({ |
| 'success': True, |
| 'content': cleaned if len(cleaned) > 50 else pollinations_response |
| }) |
| except Exception as poll_err: |
| print(f"Pollinations fallback attempt {attempt + 1} failed: {poll_err}") |
| if attempt < max_retries - 1: |
| import time |
| time.sleep(2) |
| |
| |
| error_msg = f"AI generation failed after {max_retries} attempts. Last error: {last_error}" |
| print(f"β {error_msg}") |
| return jsonify({'error': error_msg}), 500 |
|
|
| @app.route('/api/docs/create', methods=['POST']) |
| def docs_create(): |
| data = request.json |
| doc_type = data.get('doc_type', 'word') |
| content = data.get('content', '') |
| filename = data.get('filename', 'document') |
| template_id = data.get('template_id') |
| |
| if not content: |
| return jsonify({'error': 'No content provided'}), 400 |
| |
| try: |
| output_path = doc_creator.create_document(content, doc_type, filename, template_id) |
| |
| if output_path and output_path.exists(): |
| download_url = f'/api/docs/download/{output_path.name}' |
| return jsonify({ |
| 'success': True, |
| 'filename': output_path.name, |
| 'download_url': download_url |
| }) |
| else: |
| return jsonify({'error': 'Failed to create document'}), 500 |
| |
| except Exception as e: |
| print(f"Error creating document: {e}") |
| import traceback |
| traceback.print_exc() |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/docs/download/<filename>', methods=['GET']) |
| def docs_download(filename): |
| file_path = doc_creator.output_dir / filename |
| if file_path.exists(): |
| return send_file( |
| file_path, |
| as_attachment=True, |
| download_name=filename, |
| mimetype='application/octet-stream' |
| ) |
| return jsonify({'error': 'File not found'}), 404 |
|
|
| @app.route('/api/docs/extract', methods=['POST']) |
| def docs_extract(): |
| if 'file' not in request.files: |
| return jsonify({'error': 'No file uploaded'}), 400 |
| |
| file = request.files['file'] |
| extract_type = request.form.get('extract_type', 'text') |
| |
| if file.filename == '': |
| return jsonify({'error': 'No file selected'}), 400 |
| |
| filename = secure_filename(file.filename) |
| file_content = file.read() |
| |
| try: |
| temp_path = doc_creator.temp_dir / filename |
| with open(temp_path, 'wb') as f: |
| f.write(file_content) |
| |
| extracted_content = "" |
| suffix = os.path.splitext(filename)[1].lower() |
| |
| if suffix in ['.docx', '.doc']: |
| if extract_type == 'text': |
| result = doc_processor.read_word_document(str(temp_path)) |
| extracted_content = '\n'.join(result['paragraphs']) |
| elif extract_type == 'metadata': |
| result = doc_processor.read_word_document(str(temp_path)) |
| extracted_content = f"Paragraphs: {result['metadata']['paragraph_count']}\nTables: {result['metadata']['table_count']}" |
| else: |
| extracted_content = doc_processor.extract_text_from_file(str(temp_path)) |
| |
| elif suffix == '.pdf': |
| if extract_type == 'text': |
| result = doc_processor.read_pdf(str(temp_path), extract_tables=False) |
| extracted_content = '\n'.join([page['text'] for page in result['pages']]) |
| elif extract_type == 'metadata': |
| result = doc_processor.read_pdf(str(temp_path), extract_tables=False) |
| extracted_content = f"Pages: {result['page_count']}\nMetadata: {result.get('metadata', {})}" |
| else: |
| extracted_content = doc_processor.extract_text_from_file(str(temp_path)) |
| |
| elif suffix in ['.pptx', '.ppt']: |
| result = doc_processor.read_presentation(str(temp_path)) |
| if extract_type == 'slides': |
| for i, slide in enumerate(result['slides']): |
| extracted_content += f"\n--- Slide {i+1} ---\n{slide['text_content']}\n" |
| elif extract_type == 'notes': |
| for i, slide in enumerate(result['slides']): |
| if slide.get('notes'): |
| extracted_content += f"\n--- Slide {i+1} Notes ---\n{slide['notes']}\n" |
| else: |
| for slide in result['slides']: |
| extracted_content += slide['text_content'] + '\n' |
| |
| elif suffix in ['.xlsx', '.xls', '.csv']: |
| import pandas as pd |
| if suffix == '.csv': |
| df = pd.read_csv(str(temp_path)) |
| else: |
| df = pd.read_excel(str(temp_path)) |
| extracted_content = df.to_string() |
| |
| elif suffix in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']: |
| extracted_content = doc_processor.extract_text_from_image(str(temp_path)) |
| |
| elif suffix in ['.mp4', '.avi', '.mov', '.mkv']: |
| info = doc_processor.extract_video_info(str(temp_path)) |
| extracted_content = json.dumps(info, indent=2) |
| |
| else: |
| extracted_content = doc_processor.extract_text_from_file(str(temp_path)) |
| |
| return jsonify({ |
| 'success': True, |
| 'content': extracted_content[:50000], |
| 'filename': filename |
| }) |
| |
| except Exception as e: |
| print(f"Error extracting: {e}") |
| import traceback |
| traceback.print_exc() |
| return jsonify({'error': str(e)}), 500 |
| finally: |
| if os.path.exists(temp_path): |
| os.unlink(temp_path) |
|
|
| @app.route('/api/docs/merge', methods=['POST']) |
| def docs_merge(): |
| files = request.files.getlist('files') |
| output_name = request.form.get('output_name', 'merged_document') |
| |
| if len(files) < 2: |
| return jsonify({'error': 'Need at least 2 files to merge'}), 400 |
| |
| try: |
| temp_files = [] |
| for file in files: |
| if file.filename and file.filename.lower().endswith('.pdf'): |
| temp_path = doc_creator.temp_dir / secure_filename(file.filename) |
| file.save(str(temp_path)) |
| temp_files.append(str(temp_path)) |
| |
| output_filename = f"{output_name}.pdf" |
| result_path = doc_processor.merge_pdfs(temp_files, output_filename) |
| |
| for temp_file in temp_files: |
| if os.path.exists(temp_file): |
| os.unlink(temp_file) |
| |
| download_url = f'/api/docs/download/{output_filename}' |
| return jsonify({ |
| 'success': True, |
| 'filename': output_filename, |
| 'download_url': download_url |
| }) |
| |
| except Exception as e: |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/docs/split', methods=['POST']) |
| def docs_split(): |
| if 'file' not in request.files: |
| return jsonify({'error': 'No file uploaded'}), 400 |
| |
| file = request.files['file'] |
| pages_per_file = int(request.form.get('pages_per_file', 1)) |
| |
| if file.filename == '': |
| return jsonify({'error': 'No file selected'}), 400 |
| |
| if not file.filename.lower().endswith('.pdf'): |
| return jsonify({'error': 'File must be PDF'}), 400 |
| |
| try: |
| temp_path = doc_creator.temp_dir / secure_filename(file.filename) |
| file.save(str(temp_path)) |
| |
| split_dir = doc_creator.temp_dir / 'split_output' |
| split_files = doc_processor.split_pdf(str(temp_path), str(split_dir), pages_per_file) |
| |
| result_files = [] |
| for split_file in split_files: |
| filename = os.path.basename(split_file) |
| result_files.append({ |
| 'name': filename, |
| 'url': f'/api/docs/download/{filename}' |
| }) |
| import shutil |
| shutil.move(split_file, str(doc_creator.output_dir / filename)) |
| |
| os.unlink(temp_path) |
| |
| return jsonify({ |
| 'success': True, |
| 'files_count': len(result_files), |
| 'files': result_files |
| }) |
| |
| except Exception as e: |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/docs/convert', methods=['POST']) |
| def docs_convert(): |
| if 'file' not in request.files: |
| return jsonify({'error': 'No file uploaded'}), 400 |
| |
| file = request.files['file'] |
| to_format = request.form.get('to_format', 'txt') |
| |
| if file.filename == '': |
| return jsonify({'error': 'No file selected'}), 400 |
| |
| filename = secure_filename(file.filename) |
| file_content = file.read() |
| |
| try: |
| temp_path = doc_creator.temp_dir / filename |
| with open(temp_path, 'wb') as f: |
| f.write(file_content) |
| |
| ext_map = { |
| 'txt': '.txt', 'docx': '.docx', 'pdf': '.pdf', |
| 'xlsx': '.xlsx', 'csv': '.csv', 'html': '.html', |
| 'jpg': '.jpeg', 'png': '.png', 'mp3': '.mp3' |
| } |
| output_ext = ext_map.get(to_format, '.txt') |
| output_filename = f"{os.path.splitext(filename)[0]}{output_ext}" |
| |
| output_path = doc_processor.convert_document(str(temp_path), output_filename, output_format=to_format) |
| |
| os.unlink(temp_path) |
| |
| download_url = f'/api/docs/download/{os.path.basename(output_path)}' |
| return jsonify({ |
| 'success': True, |
| 'filename': os.path.basename(output_path), |
| 'format': to_format, |
| 'download_url': download_url |
| }) |
| |
| except Exception as e: |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/docs/resize', methods=['POST']) |
| def docs_resize(): |
| if 'file' not in request.files: |
| return jsonify({'error': 'No file uploaded'}), 400 |
| |
| file = request.files['file'] |
| width = request.form.get('width') |
| height = request.form.get('height') |
| maintain_aspect = request.form.get('maintain_aspect', 'true').lower() == 'true' |
| |
| if file.filename == '': |
| return jsonify({'error': 'No file selected'}), 400 |
| |
| filename = secure_filename(file.filename) |
| file_content = file.read() |
| |
| try: |
| temp_path = doc_creator.temp_dir / filename |
| with open(temp_path, 'wb') as f: |
| f.write(file_content) |
| |
| w = int(width) if width else None |
| h = int(height) if height else None |
| output_filename = f"resized_{filename}" |
| |
| output_path = doc_processor.resize_image( |
| str(temp_path), output_filename, |
| width=w, height=h, maintain_aspect=maintain_aspect |
| ) |
| |
| from PIL import Image |
| img = Image.open(output_path) |
| new_width, new_height = img.size |
| |
| os.unlink(temp_path) |
| |
| download_url = f'/api/docs/download/{output_filename}' |
| return jsonify({ |
| 'success': True, |
| 'filename': output_filename, |
| 'width': new_width, |
| 'height': new_height, |
| 'download_url': download_url |
| }) |
| |
| except Exception as e: |
| return jsonify({'error': str(e)}), 500 |
|
|
| |
| register_workspace_routes(app) |
|
|
| @app.route('/api/generated_image/<filename>') |
| @require_auth |
| def serve_generated_image(filename): |
| from flask import send_from_directory |
| import os |
| |
| if '..' in filename or filename.startswith('/'): |
| return jsonify({'error': 'Invalid filename'}), 400 |
| |
| |
| user_id = get_current_user_id() |
| user_image_path = get_user_image_path(user_id, filename) |
| |
| if user_image_path and user_image_path.exists(): |
| return send_from_directory(str(user_image_path.parent), filename) |
| |
| |
| global_path = os.path.join(GENERATED_IMAGES_DIR, filename) |
| if os.path.exists(global_path): |
| return send_from_directory(GENERATED_IMAGES_DIR, filename) |
| |
| return jsonify({'error': 'Image not found'}), 404 |
|
|
| |
| |
| |
|
|
| @app.route('/admin') |
| @require_auth |
| def admin_index(): |
| """Admin panel - requires admin authentication from proxy""" |
| if not is_admin(): |
| return jsonify({'error': 'Admin access required'}), 403 |
| return render_template('admin_dashboard.html') |
|
|
|
|
| @app.route('/admin/api/stats') |
| @require_auth |
| def admin_api_stats(): |
| """Get system statistics for admin dashboard""" |
| if not is_admin(): |
| return jsonify({'error': 'Admin access required'}), 403 |
| |
| user_dir = '/data/users' |
| total_users = 0 |
| total_conversations = 0 |
| |
| if os.path.exists(user_dir): |
| for user_id in os.listdir(user_dir): |
| total_users += 1 |
| conv_path = os.path.join(user_dir, user_id, 'conversations.json') |
| if os.path.exists(conv_path): |
| try: |
| with open(conv_path, 'r') as f: |
| convs = json.load(f) |
| total_conversations += len(convs) |
| except: |
| pass |
| |
| return jsonify({ |
| 'success': True, |
| 'stats': { |
| 'total_users': total_users, |
| 'total_conversations': total_conversations, |
| 'total_workspace_files': 0, |
| 'total_generated_images': 0 |
| } |
| }) |
|
|
|
|
| @app.route('/admin/api/users') |
| @require_auth |
| def admin_api_users(): |
| """Get list of all users for admin dashboard""" |
| if not is_admin(): |
| return jsonify({'error': 'Admin access required'}), 403 |
| |
| user_dir = '/data/users' |
| users = [] |
| |
| if os.path.exists(user_dir): |
| for user_id in os.listdir(user_dir): |
| profile_path = os.path.join(user_dir, user_id, 'profile.json') |
| if os.path.exists(profile_path): |
| try: |
| with open(profile_path, 'r') as f: |
| profile = json.load(f) |
| users.append({ |
| 'user_id': user_id, |
| 'username': profile.get('username', user_id[:8]), |
| 'email': profile.get('email', ''), |
| 'created_at': profile.get('created_at', ''), |
| 'last_active': profile.get('last_active', '') |
| }) |
| except: |
| pass |
| |
| return jsonify({'success': True, 'users': users}) |
|
|
|
|
| @app.route('/admin/api/users/<user_id>', methods=['DELETE']) |
| @require_auth |
| def admin_api_delete_user(user_id): |
| """Delete a user and all their data""" |
| if not is_admin(): |
| return jsonify({'error': 'Admin access required'}), 403 |
| |
| import shutil |
| user_dir = f'/data/users/{user_id}' |
| |
| if os.path.exists(user_dir): |
| shutil.rmtree(user_dir) |
| return jsonify({'success': True}) |
| |
| return jsonify({'success': False, 'error': 'User not found'}), 404 |
|
|
| if __name__ == '__main__': |
| print("\n" + "="*60) |
| print("π HenAi Server Started!") |
| print("="*60) |
| print("π Visit: http://localhost:5000") |
| print("π€ AI Mode: Hybrid - Pollinations.ai for conversations, OpenRouter for code") |
| print(" π Conversations/Explanations: Pollinations.ai (fast, prioritized)") |
| print(" π» Code Generation: OpenRouter only (with multiple free models)") |
| print("πΌοΈ Media Search: Images & Videos from Pixabay and Pexels") |
| print("π¬ Video Analysis: TwelveLabs API") |
| print("π‘ Commands: /search, /extract, /code, /image, /help") |
| print("π Features: Copy, Edit, Regenerate, Version Toggle, Rename, Export") |
| print("π File Support: Text, Code, Documents (PDF, DOCX, etc.)") |
| print("π Document Creation: Word, Excel, PowerPoint, PDF, Images") |
| print("π Branching: Full version history with sibling and child branches") |
| print("="*60 + "\n") |
| |
| port = int(os.environ.get('PORT', 7860)) |
| app.run(host='0.0.0.0', port=port, debug=False) |