Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 46 additions & 22 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +272 to +287
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The history-fetch/conversation-resolution block swallows all exceptions and then proceeds, which can silently disable both history and DB logging for logged-in users (e.g., non-integer conversation_id, DB errors). Since this code is critical to the new “AI has memory” behavior, consider handling expected errors (like ValueError) with a fallback to the active conversation and logging unexpected exceptions so failures are visible.

Copilot uses AI. Check for mistakes.

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
Expand Down Expand Up @@ -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():
Expand Down
30 changes: 30 additions & 0 deletions login/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 14 additions & 10 deletions processing/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 `'
}
Comment on lines +31 to 36
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user_name comes from user-controlled profile data and is interpolated directly into the system prompt. This enables prompt-injection at the highest priority (system) level (e.g., a display name containing instructions/newlines/backticks), which can undermine safety constraints and may increase the risk of the model emitting unsafe HTML that is later marked safe via Markup(). Consider sanitizing/normalizing the name (length limit, strip newlines/control chars, quote/escape) and/or moving the personalization into a lower-priority message instead of the system prompt.

Copilot uses AI. Check for mistakes.

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})
Comment on lines +38 to +46
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

send() now appends the entire conversation history to the Ollama prompt. Since get_conversation_messages() returns all messages in a conversation, prompts can grow without bound and eventually exceed the model context window or cause very slow requests. Consider truncating/summarizing history (e.g., last N turns or last N characters/tokens) before calling Ollama, ideally using a configurable limit.

Copilot uses AI. Check for mistakes.

try:
response = ollama.chat(model=ai_model, messages=messages)
return response['message']['content']
Expand Down
120 changes: 120 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -586,6 +635,22 @@
<div class="sidebar-brand">
<h5><i class="fas fa-robot me-2"></i>Komli</h5>
</div>

{% if user %}
<!-- Search -->
<div class="sidebar-search">
<div class="sidebar-search-wrapper">
<input type="text" class="sidebar-search-input" id="sidebarSearch"
placeholder="Search conversations..."
oninput="handleSearchInput(this.value)">
<i class="fas fa-search sidebar-search-icon"></i>
</div>
<!-- Search Results -->
<div class="search-results-section" id="searchResultsSection" style="display: none;">
<div id="searchResultsList"></div>
</div>
</div>
{% endif %}

<nav class="nav flex-column">
<a href="#" class="nav-link active" onclick="startNewChat(event)">
Expand Down Expand Up @@ -701,6 +766,8 @@ <h1 id="chatTitle"><i class="fas fa-robot me-3"></i>Komli</h1>
<script>
// Global variables
const userLoggedIn = {% if user %}true{% else %}false{% endif %};
const SEARCH_DEBOUNCE_MS = 300;
const MAX_SEARCH_RESULT_TITLE_LENGTH = 22;

function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
Expand Down Expand Up @@ -1071,6 +1138,59 @@ <h1 id="chatTitle"><i class="fas fa-robot me-3"></i>Komli</h1>
toggleSidebar();
}
}

// Search functionality
let searchTimeout = null;

function handleSearchInput(query) {
const resultsSection = document.getElementById('searchResultsSection');
clearTimeout(searchTimeout);

if (!query.trim()) {
resultsSection.style.display = 'none';
return;
}

// Debounce: wait SEARCH_DEBOUNCE_MS after the user stops typing
searchTimeout = setTimeout(() => performSearch(query.trim()), SEARCH_DEBOUNCE_MS);
}

function performSearch(query) {
const resultsSection = document.getElementById('searchResultsSection');
const resultsList = document.getElementById('searchResultsList');

resultsSection.style.display = 'block';
resultsList.innerHTML = '<div class="text-center py-2"><i class="fas fa-spinner fa-spin"></i></div>';

fetch('/api/search?q=' + encodeURIComponent(query))
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search UI calls fetch('/api/search?...') using an absolute root path. The backend routes are prefixed by web_dir (configurable) and the app is explicitly designed to run under a non-root base path; this call (and any new calls) will break when web_dir isn’t /. Consider using a base path value rendered from the template (or switching to a relative URL) so the request is correctly scoped under web_dir.

Suggested change
fetch('/api/search?q=' + encodeURIComponent(query))
fetch('api/search?q=' + encodeURIComponent(query))

Copilot uses AI. Check for mistakes.
.then(response => response.json())
.then(data => {
resultsList.innerHTML = '';
if (data.results && data.results.length > 0) {
data.results.forEach(conv => {
const item = document.createElement('div');
item.className = 'conversation-item';
item.onclick = () => {
loadConversation(conv.id);
document.getElementById('sidebarSearch').value = '';
resultsSection.style.display = 'none';
};
const title = conv.title || `Chat #${conv.id}`;
const truncatedTitle = title.length > MAX_SEARCH_RESULT_TITLE_LENGTH ? title.substring(0, MAX_SEARCH_RESULT_TITLE_LENGTH) + '...' : title;
item.innerHTML = `
<div class="conversation-title">${escapeHtml(truncatedTitle)}</div>
<div class="conversation-meta">${conv.message_count} messages</div>
`;
resultsList.appendChild(item);
});
} else {
resultsList.innerHTML = '<div class="text-center py-2"><small class="text-muted">No results found</small></div>';
}
})
.catch(() => {
resultsList.innerHTML = '<div class="text-center py-2"><small class="text-muted">Search failed</small></div>';
});
}
</script>
</body>
</html>
Loading