From e491e276fc76b1bb55399acbcef44fd8590993bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:47:59 +0000 Subject: [PATCH 1/2] Initial plan From 656fb9bb7a7ae3e939f97f41461afef8a2f19931 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:54:11 +0000 Subject: [PATCH 2/2] Add AI chat history, conversation search, and per-user personalization Co-authored-by: QuickMash <106212829+QuickMash@users.noreply.github.com> --- app.py | 68 ++++++++++++++++-------- login/server.py | 30 +++++++++++ processing/ai.py | 24 +++++---- templates/index.html | 120 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 32 deletions(-) diff --git a/app.py b/app.py index 3d323b2..79c0710 100644 --- a/app.py +++ b/app.py @@ -255,43 +255,50 @@ def update_profile(): def respond(): user_input = request.form.get('user_input') conversation_id = request.form.get('conversation_id') # Optional conversation ID - + if not user_input: return jsonify({"error": "No input provided"}), 400 - + # Get user data if logged in user_data = get_authenticated_user() user_email = user_data['email'] if user_data else None user_id = user_data['id'] if user_data else None - + user_name = user_data['name'] if user_data else None + + # Resolve the target conversation and retrieve its history before calling the AI + history = [] + target_conversation_id = None + + if user_email: + try: + if conversation_id: + conversation_record = server.get_conversation(int(conversation_id), user_email) + if conversation_record: + target_conversation_id = int(conversation_id) + else: + target_conversation_id = server.get_or_create_active_conversation(user_email) + else: + target_conversation_id = server.get_or_create_active_conversation(user_email) + + # Fetch existing messages so the AI sees the full conversation history + if target_conversation_id: + history = server.get_conversation_messages(target_conversation_id) + except Exception: + pass + try: ai.modTokens(str(user_id) if user_id else "guest", user_input) - system_response = ai.send(user_input) + system_response = ai.send(user_input, history=history, user_name=user_name) markdown_response = md.convert(system_response) - + # Log the conversation if user is logged in - if user_email: + if user_email and target_conversation_id: try: - # Use provided conversation_id or get/create active conversation - if conversation_id: - # Verify the conversation belongs to the user - conversation_record = server.get_conversation(int(conversation_id), user_email) - if conversation_record: - target_conversation_id = int(conversation_id) - else: - target_conversation_id = server.get_or_create_active_conversation(user_email) - else: - target_conversation_id = server.get_or_create_active_conversation(user_email) - - # Add user message server.add_message(target_conversation_id, user_email, 'user', user_input) - - # Add assistant response server.add_message(target_conversation_id, user_email, 'assistant', system_response) - except Exception: pass - + return Markup(markdown_response) except Exception as error: return jsonify({"error": f"Failed: {error}"}), 500 @@ -346,6 +353,23 @@ def api_conversation_messages(conversation_id): return jsonify({"messages": messages}) +# API endpoint for searching conversations +@app.route(f'{web_dir}/api/search') +def api_search(): + # Check if user is logged in + user_data = get_authenticated_user() + + if not user_data: + return jsonify({"error": "Not authenticated"}), 401 + + query = request.args.get('q', '').strip() + + if not query: + return jsonify({"results": []}) + + results = server.search_conversations(user_data['email'], query) + return jsonify({"results": results}) + # For a random background @app.route(f'{web_dir}/background.jpg') def background(): diff --git a/login/server.py b/login/server.py index b8188b3..3615168 100644 --- a/login/server.py +++ b/login/server.py @@ -409,6 +409,36 @@ def get_conversation_messages(conversation_id): db.close() return [] +def search_conversations(user_email, query): + """Searches conversations and messages for a given user by keyword.""" + db = sqlite3.connect(DATABASE) + cursor = db.cursor() + try: + # Escape LIKE wildcards so user input is treated as a literal string + escaped = query.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_') + like_query = f'%{escaped}%' + cursor.execute(""" + SELECT DISTINCT c.id, c.title, c.updated_at, + (SELECT COUNT(*) FROM messages WHERE conversation_id = c.id) as message_count + FROM conversations c + LEFT JOIN messages m ON c.id = m.conversation_id + WHERE c.user_email = ? + AND (c.title LIKE ? ESCAPE '\\' OR m.content LIKE ? ESCAPE '\\') + ORDER BY c.updated_at DESC + LIMIT 20 + """, (user_email, like_query, like_query)) + results = cursor.fetchall() + db.close() + return [{ + 'id': r[0], + 'title': r[1], + 'updated_at': r[2], + 'message_count': r[3] + } for r in results] + except Exception: + db.close() + return [] + app = Flask(__name__) @app.teardown_appcontext diff --git a/processing/ai.py b/processing/ai.py index 8a3c7ac..cbc6002 100644 --- a/processing/ai.py +++ b/processing/ai.py @@ -28,19 +28,23 @@ def modTokens(user: str, user_input: str) -> None: """Backward-compatible alias for mod_tokens.""" return mod_tokens(user, user_input) -def send(user_input: str) -> str: +def send(user_input: str, history: list = None, user_name: str = None) -> str: + name_part = f" You are talking to {user_name}." if user_name else "" system_message = { 'role': 'system', - 'content': f'You are {name}. {sys_prompt} Version: {version}. You are only allowed to speak with markdown formatting. Begin normal messages with ` and end them with `' + 'content': f'You are {name}. {sys_prompt}{name_part} Version: {version}. You are only allowed to speak with markdown formatting. Begin normal messages with ` and end them with `' } - - user_message = { - 'role': 'user', - 'content': user_input - } - - messages = [system_message, user_message] - + + messages = [system_message] + + # Include prior conversation turns so the AI has memory of the chat + if history: + for msg in history: + role = 'user' if msg.get('message_type') == 'user' else 'assistant' + messages.append({'role': role, 'content': msg['content']}) + + messages.append({'role': 'user', 'content': user_input}) + try: response = ollama.chat(model=ai_model, messages=messages) return response['message']['content'] diff --git a/templates/index.html b/templates/index.html index 2e26e49..f61c396 100644 --- a/templates/index.html +++ b/templates/index.html @@ -371,6 +371,55 @@ color: white; } + /* Search Input Styles */ + .sidebar-search { + margin-bottom: 16px; + } + + .sidebar-search-input { + width: 100%; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + padding: 8px 36px 8px 12px; + color: white; + font-size: 0.875rem; + outline: none; + transition: all 0.3s ease; + } + + .sidebar-search-input::placeholder { + color: rgba(255, 255, 255, 0.5); + } + + .sidebar-search-input:focus { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.4); + } + + .sidebar-search-wrapper { + position: relative; + } + + .sidebar-search-icon { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + color: rgba(255, 255, 255, 0.5); + font-size: 0.8rem; + pointer-events: none; + } + + .search-results-section { + margin-left: 16px; + border-left: 2px solid rgba(255, 255, 255, 0.1); + padding-left: 16px; + margin-top: 4px; + max-height: 240px; + overflow-y: auto; + } + /* Chat History Sub-section Styles */ .chat-history-section { margin-left: 16px; @@ -586,6 +635,22 @@ + + {% if user %} + + + {% endif %}