diff --git a/backend/static/js/api-client.js b/backend/static/js/api-client.js index d2a2b61..e9f04b6 100644 --- a/backend/static/js/api-client.js +++ b/backend/static/js/api-client.js @@ -372,7 +372,7 @@ class ApiClient { async sendMessage(conversationId, message) { return this.post(`/api/ai-chat/conversations/${conversationId}/messages`, { - message + content: message }); } diff --git a/backend/static/js/apps.js b/backend/static/js/apps.js index 448690b..d5f2f13 100644 --- a/backend/static/js/apps.js +++ b/backend/static/js/apps.js @@ -7,6 +7,13 @@ class BlackRoadApps { constructor() { this.api = window.BlackRoadAPI; this.refreshIntervals = {}; + this.aiChatState = { + conversations: [], + activeConversationId: null, + messages: [], + loadingMessages: false, + sendingMessage: false, + }; } /** @@ -578,30 +585,254 @@ class BlackRoadApps { if (!content) return; content.innerHTML = ` -
-
-
AI Assistant ready! How can I help you?
+
+
+
+

Conversations

+ +
+
+
Loading conversations...
+
-
- - +
+
+
Loading conversations...
+
+
+ + +
`; + + await this.fetchAIConversations({ selectFirst: true }); + if (this.aiChatState.activeConversationId) { + await this.fetchAIMessages(this.aiChatState.activeConversationId); + } else { + this.updateAIChatMessagesUI(); + } + } + + async fetchAIConversations({ selectFirst = false } = {}) { + const container = document.getElementById('ai-chat-conversations'); + if (container && !this.aiChatState.conversations.length) { + container.innerHTML = '
Loading conversations...
'; + } + + try { + const conversations = await this.api.getConversations(); + this.aiChatState.conversations = conversations; + + if (selectFirst && conversations.length && !this.aiChatState.activeConversationId) { + this.aiChatState.activeConversationId = conversations[0].id; + } + + if (this.aiChatState.activeConversationId) { + const exists = conversations.some(conv => conv.id === this.aiChatState.activeConversationId); + if (!exists) { + this.aiChatState.activeConversationId = conversations[0]?.id || null; + } + } + + this.updateAIChatConversationsUI(); + } catch (error) { + console.error('Failed to load AI chat conversations:', error); + if (container) { + container.innerHTML = `
${this.escapeHtml(error.message || 'Unable to load conversations')}
`; + } + } + } + + updateAIChatConversationsUI() { + const container = document.getElementById('ai-chat-conversations'); + if (!container) return; + + const { conversations, activeConversationId } = this.aiChatState; + + if (!conversations.length) { + container.innerHTML = '
No conversations yet. Create one to start chatting.
'; + return; + } + + container.innerHTML = conversations.map(convo => ` +
+
${this.escapeHtml(convo.title || 'Untitled')}
+
${convo.message_count || 0} messages
+
+ `).join(''); + } + + async selectAIConversation(conversationId) { + if (this.aiChatState.activeConversationId === conversationId && !this.aiChatState.loadingMessages) { + return; + } + + this.aiChatState.activeConversationId = conversationId; + this.updateAIChatConversationsUI(); + await this.fetchAIMessages(conversationId); + } + + async createAIConversation() { + try { + const conversation = await this.api.createConversation('New Conversation'); + this.aiChatState.conversations = [conversation, ...this.aiChatState.conversations]; + this.aiChatState.activeConversationId = conversation.id; + this.aiChatState.messages = []; + this.updateAIChatConversationsUI(); + this.updateAIChatMessagesUI(); + const input = document.getElementById('ai-chat-input'); + if (input) input.focus(); + return conversation; + } catch (error) { + console.error('Failed to create AI conversation:', error); + const container = document.getElementById('ai-chat-conversations'); + if (container) { + container.insertAdjacentHTML('afterbegin', `
${this.escapeHtml(error.message || 'Unable to create conversation')}
`); + } + return null; + } + } + + async fetchAIMessages(conversationId) { + const messagesContainer = document.getElementById('ai-chat-messages'); + if (messagesContainer) { + messagesContainer.innerHTML = '
Loading messages...
'; + } + + if (!conversationId) { + this.aiChatState.messages = []; + this.updateAIChatMessagesUI(); + return; + } + + this.aiChatState.loadingMessages = true; + + try { + const messages = await this.api.getMessages(conversationId); + this.aiChatState.messages = messages; + this.updateAIChatMessagesUI(); + } catch (error) { + console.error('Failed to load AI chat messages:', error); + if (messagesContainer) { + messagesContainer.innerHTML = `
${this.escapeHtml(error.message || 'Unable to load messages')}
`; + } + } finally { + this.aiChatState.loadingMessages = false; + } + } + + updateAIChatMessagesUI() { + const container = document.getElementById('ai-chat-messages'); + if (!container) return; + + const { activeConversationId, messages } = this.aiChatState; + + if (!activeConversationId) { + container.innerHTML = '
Select a conversation or create a new one to start chatting.
'; + return; + } + + if (!messages.length) { + container.innerHTML = '
No messages yet. Say hello!
'; + return; + } + + container.innerHTML = messages.map(message => ` +
+
+ ${message.role === 'assistant' ? 'AI Assistant' : 'You'} +
+
+ ${this.escapeHtml(message.content)} +
+
+ `).join(''); + + this.scrollAIChatToBottom(); + } + + scrollAIChatToBottom() { + const container = document.getElementById('ai-chat-messages'); + if (container) { + container.scrollTop = container.scrollHeight; + } } async sendAIMessage() { const input = document.getElementById('ai-chat-input'); - const message = input.value.trim(); - if (!message) return; + const sendBtn = document.getElementById('ai-chat-send-btn'); + if (!input) return; - console.log('Send AI message:', message); - // TODO: Implement AI chat - input.value = ''; + const message = input.value.trim(); + if (!message || this.aiChatState.sendingMessage) return; + + this.aiChatState.sendingMessage = true; + if (sendBtn) { + sendBtn.disabled = true; + sendBtn.textContent = 'Sending...'; + } + + let conversationId = this.aiChatState.activeConversationId; + + try { + if (!conversationId) { + const conversation = await this.createAIConversation(); + conversationId = conversation?.id; + } + + if (!conversationId) { + throw new Error('Unable to start a new conversation'); + } + + // Optimistic user message + this.aiChatState.messages = [ + ...this.aiChatState.messages, + { + id: `temp-${Date.now()}`, + role: 'user', + content: message, + created_at: new Date().toISOString() + } + ]; + this.updateAIChatMessagesUI(); + input.value = ''; + + await this.api.sendMessage(conversationId, message); + await this.fetchAIMessages(conversationId); + await this.fetchAIConversations(); + } catch (error) { + console.error('Failed to send AI chat message:', error); + const messagesContainer = document.getElementById('ai-chat-messages'); + if (messagesContainer) { + messagesContainer.insertAdjacentHTML('beforeend', `
${this.escapeHtml(error.message || 'Failed to send message')}
`); + } + } finally { + this.aiChatState.sendingMessage = false; + if (sendBtn) { + sendBtn.disabled = false; + sendBtn.textContent = 'Send'; + } + } } // ===== UTILITY FUNCTIONS ===== + escapeHtml(value) { + if (typeof value !== 'string') return ''; + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + return value.replace(/[&<>"']/g, (char) => map[char]); + } + formatTime(timestamp) { const date = new Date(timestamp); const now = new Date();