diff --git a/static/app.js b/static/app.js
index 5c941f0..b17a5a7 100644
--- a/static/app.js
+++ b/static/app.js
@@ -300,11 +300,283 @@ function showToast(message, type) {
}, 4000);
}
-// ── Event listeners ──────────────────────────────────────────────────
+// ── Agent Chat ────────────────────────────────────────────────────
+
+let chatState = {
+ profiles: [],
+ activeProfile: null,
+ messages: {},
+ sending: false,
+};
+
+const PROFILE_ICONS = {
+ coder: '💻',
+ assistant: '🤖',
+ artist: '🎨',
+ kanban: '📋',
+};
+
+async function loadAgentProfiles() {
+ try {
+ const res = await fetch('/api/agents');
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = await res.json();
+ chatState.profiles = data.agents || [];
+ renderProfileTabs();
+ // If we have an active profile, re-check its status
+ if (chatState.activeProfile) {
+ selectProfile(chatState.activeProfile);
+ } else if (chatState.profiles.length > 0) {
+ // Auto-select first online profile, or first profile
+ const online = chatState.profiles.find(p => p.status === 'online');
+ selectProfile(online || chatState.profiles[0]);
+ }
+ } catch (err) {
+ console.error('Failed to load profiles:', err);
+ }
+}
+
+function renderProfileTabs() {
+ const container = document.getElementById('chat-profile-tabs');
+ container.innerHTML = '';
+ chatState.profiles.forEach(p => {
+ const tab = document.createElement('button');
+ tab.className = 'chat-profile-tab';
+ if (p.profile === chatState.activeProfile?.profile) {
+ tab.classList.add('active');
+ }
+ const icon = PROFILE_ICONS[p.profile] || '🤖';
+ tab.innerHTML = `
+
+ ${icon} ${p.label || p.profile}
+ `;
+ tab.title = p.status === 'online' ? 'Online' : 'Offline';
+ tab.addEventListener('click', () => selectProfile(p));
+ container.appendChild(tab);
+ });
+}
+
+function selectProfile(profile) {
+ chatState.activeProfile = profile;
+ // Update tab UI
+ document.querySelectorAll('.chat-profile-tab').forEach(tab => {
+ const txt = tab.textContent.trim();
+ tab.classList.toggle('active', txt.includes(profile.label || profile.profile));
+ });
+ // Clear messages area
+ const msgArea = document.getElementById('chat-messages');
+ msgArea.innerHTML = '';
+ // Show welcome if no history, or loading
+ if (!chatState.messages[profile.profile]) {
+ showWelcome(profile);
+ }
+ // Enable input
+ document.getElementById('chat-input').disabled = false;
+ document.getElementById('chat-input').focus();
+ updateSendButton();
+ // Load history
+ loadConversationHistory(profile.profile);
+}
+
+function showWelcome(profile) {
+ const msgArea = document.getElementById('chat-messages');
+ const icon = PROFILE_ICONS[profile.profile] || '🤖';
+ msgArea.innerHTML = `
+
+
${icon}
+
${profile.label || profile.profile}
+
${profile.status === 'online'
+ ? 'This agent is online. Type a message to start a conversation.'
+ : 'This agent is offline. Start the gateway to chat.'}
+
+ `;
+}
+
+async function loadConversationHistory(profileName) {
+ try {
+ const res = await fetch(`/api/agents/${encodeURIComponent(profileName)}/history?limit=50`);
+ if (!res.ok) return;
+ const data = await res.json();
+ const messages = data.messages || [];
+ chatState.messages[profileName] = messages;
+
+ // Only render if this is still the active profile
+ if (chatState.activeProfile?.profile === profileName) {
+ renderMessages(messages);
+ }
+ } catch (err) {
+ console.error('Failed to load history:', err);
+ }
+}
+
+function renderMessages(messages) {
+ const msgArea = document.getElementById('chat-messages');
+ msgArea.innerHTML = '';
+ if (!messages || messages.length === 0) {
+ if (chatState.activeProfile) {
+ showWelcome(chatState.activeProfile);
+ }
+ return;
+ }
+ messages.forEach(msg => appendMessageBubble(msg, false));
+ scrollToBottom();
+}
+
+function appendMessageBubble(msg, animate = true) {
+ const msgArea = document.getElementById('chat-messages');
+ const isUser = msg.role === 'user';
+
+ const div = document.createElement('div');
+ div.className = `chat-message ${isUser ? 'user' : 'agent'}`;
+ if (!animate) div.style.animation = 'none';
+
+ const icon = chatState.activeProfile
+ ? (PROFILE_ICONS[chatState.activeProfile.profile] || '🤖')
+ : '🤖';
+
+ const time = msg.created_at
+ ? new Date(msg.created_at * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+ : '';
+
+ div.innerHTML = `
+ ${escapeHtml(msg.content)}
+
+ ${isUser ? '' : `${icon}`}
+ ${isUser ? 'You' : (chatState.activeProfile?.label || 'Agent')}
+ ${time ? '· ' + time + '' : ''}
+
+ `;
+
+ msgArea.appendChild(div);
+ scrollToBottom();
+}
+
+function scrollToBottom() {
+ const msgArea = document.getElementById('chat-messages');
+ msgArea.scrollTop = msgArea.scrollHeight;
+}
+
+function showTypingIndicator(show) {
+ document.getElementById('chat-typing').hidden = !show;
+}
+
+async function sendMessage() {
+ if (chatState.sending) return;
+ const input = document.getElementById('chat-input');
+ const message = input.value.trim();
+ if (!message || !chatState.activeProfile) return;
+
+ chatState.sending = true;
+ input.disabled = true;
+ document.getElementById('chat-send-btn').disabled = true;
+ document.getElementById('chat-send-btn').textContent = '...';
+
+ // Optimistically show user message
+ appendMessageBubble({ role: 'user', content: message, created_at: Date.now() / 1000 });
+ input.value = '';
+ input.style.height = 'auto';
+
+ // Show typing indicator
+ showTypingIndicator(true);
+
+ try {
+ const res = await fetch(`/api/agents/${encodeURIComponent(chatState.activeProfile.profile)}/send`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ message }),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ appendMessageBubble({
+ role: 'agent',
+ content: '⚠️ Error: ' + (data.error || `HTTP ${res.status}`),
+ created_at: Date.now() / 1000,
+ });
+ } else {
+ appendMessageBubble({
+ role: 'agent',
+ content: data.response,
+ created_at: Date.now() / 1000,
+ });
+ }
+ } catch (err) {
+ appendMessageBubble({
+ role: 'agent',
+ content: '⚠️ Network error: ' + err.message,
+ created_at: Date.now() / 1000,
+ });
+ } finally {
+ chatState.sending = false;
+ showTypingIndicator(false);
+ input.disabled = false;
+ document.getElementById('chat-send-btn').disabled = false;
+ document.getElementById('chat-send-btn').textContent = 'Send';
+ input.focus();
+ }
+}
+
+function updateSendButton() {
+ const input = document.getElementById('chat-input');
+ const btn = document.getElementById('chat-send-btn');
+ btn.disabled = !input.value.trim() || !chatState.activeProfile || chatState.sending;
+}
+
+function escapeHtml(text) {
+ if (!text) return '';
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+// ── Chat event listeners ──────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
- document.getElementById('config-save-btn').addEventListener('click', saveFile);
- document.getElementById('config-back-btn').addEventListener('click', goBackToList);
+ // Config editor listeners
+ const configSaveBtn = document.getElementById('config-save-btn');
+ const configBackBtn = document.getElementById('config-back-btn');
+ if (configSaveBtn) configSaveBtn.addEventListener('click', saveFile);
+ if (configBackBtn) configBackBtn.addEventListener('click', goBackToList);
+
+ // Network dashboard
initNetworkDashboard();
+
+ // Chat input
+ const chatInput = document.getElementById('chat-input');
+ const sendBtn = document.getElementById('chat-send-btn');
+
+ if (chatInput) {
+ chatInput.addEventListener('input', () => {
+ updateSendButton();
+ // Auto-resize
+ chatInput.style.height = 'auto';
+ chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px';
+ });
+
+ chatInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ sendMessage();
+ }
+ });
+ }
+
+ if (sendBtn) {
+ sendBtn.addEventListener('click', sendMessage);
+ }
+
+ // Load profiles when agents tab is navigated to
+ const agentsLink = document.querySelector('.nav-links a[href="#agents"]');
+ if (agentsLink) {
+ agentsLink.addEventListener('click', () => {
+ setTimeout(loadAgentProfiles, 100);
+ });
+ }
+
+ // Also load if we start on agents tab
+ if (!window.location.hash || window.location.hash === '#agents' || window.location.hash === '') {
+ setTimeout(loadAgentProfiles, 200);
+ }
});
// ── Network Dashboard ────────────────────────────────────────────────
diff --git a/static/style.css b/static/style.css
index c29c8d6..4db9765 100644
--- a/static/style.css
+++ b/static/style.css
@@ -32,6 +32,276 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
.content { margin-left: 0; padding: 12px; }
}
+/* ── Agent Chat ──────────────────────────────────────────────────── */
+#chat-app {
+ display: flex;
+ flex-direction: column;
+ height: calc(100vh - 100px);
+ min-height: 400px;
+}
+
+.chat-header {
+ flex-shrink: 0;
+ padding: 0 0 12px;
+ border-bottom: 1px solid var(--border);
+ margin-bottom: 0;
+}
+
+.chat-profile-strip {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.chat-profiles-label {
+ font-size: 12px;
+ color: var(--text-muted);
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.chat-profile-tabs {
+ display: flex;
+ gap: 4px;
+ flex-wrap: wrap;
+}
+
+.chat-profile-tab {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 12px;
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ background: transparent;
+ color: var(--text-muted);
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.15s;
+ white-space: nowrap;
+}
+
+.chat-profile-tab:hover {
+ border-color: var(--accent);
+ color: var(--text);
+}
+
+.chat-profile-tab.active {
+ background: rgba(0, 212, 255, 0.1);
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+.chat-profile-tab .status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.chat-profile-tab .status-dot.online {
+ background: var(--success);
+ box-shadow: 0 0 6px rgba(63, 185, 80, 0.5);
+}
+
+.chat-profile-tab .status-dot.offline {
+ background: var(--text-muted);
+}
+
+.chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ scroll-behavior: smooth;
+}
+
+.chat-welcome {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ flex: 1;
+ color: var(--text-muted);
+ gap: 8px;
+}
+
+.chat-welcome-icon {
+ font-size: 48px;
+ opacity: 0.5;
+}
+
+.chat-welcome h3 {
+ font-size: 18px;
+ color: var(--text);
+ margin: 0;
+}
+
+.chat-welcome p {
+ font-size: 13px;
+ max-width: 360px;
+ line-height: 1.5;
+ margin: 0;
+}
+
+.chat-message {
+ display: flex;
+ flex-direction: column;
+ max-width: 80%;
+ animation: msg-in 0.2s ease;
+}
+
+@keyframes msg-in {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.chat-message.user {
+ align-self: flex-end;
+}
+
+.chat-message.agent {
+ align-self: flex-start;
+}
+
+.chat-message .bubble {
+ padding: 10px 14px;
+ border-radius: 14px;
+ line-height: 1.5;
+ font-size: 13px;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+}
+
+.chat-message.user .bubble {
+ background: var(--accent);
+ color: #000;
+ border-bottom-right-radius: 4px;
+}
+
+.chat-message.agent .bubble {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-bottom-left-radius: 4px;
+ color: var(--text);
+}
+
+.chat-message .msg-meta {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 10px;
+ color: var(--text-muted);
+ padding: 2px 4px 0;
+}
+
+.chat-message.user .msg-meta {
+ justify-content: flex-end;
+}
+
+.chat-message .msg-profile-icon {
+ font-size: 11px;
+}
+
+.chat-typing {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 8px 4px;
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.typing-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--text-muted);
+ animation: typing-bounce 1.4s infinite ease-in-out;
+}
+
+.typing-dot:nth-child(2) { animation-delay: 0.2s; }
+.typing-dot:nth-child(3) { animation-delay: 0.4s; }
+
+@keyframes typing-bounce {
+ 0%, 60%, 100% { transform: translateY(0); }
+ 30% { transform: translateY(-6px); }
+}
+
+.typing-label {
+ margin-left: 6px;
+ font-style: italic;
+}
+
+.chat-input-area {
+ flex-shrink: 0;
+ display: flex;
+ gap: 8px;
+ padding-top: 12px;
+ border-top: 1px solid var(--border);
+}
+
+.chat-input-area textarea {
+ flex: 1;
+ padding: 10px 14px;
+ border: 1px solid var(--border);
+ border-radius: 20px;
+ background: var(--surface);
+ color: var(--text);
+ font-size: 13px;
+ font-family: inherit;
+ outline: none;
+ resize: none;
+ line-height: 1.4;
+ min-height: 40px;
+ max-height: 120px;
+ transition: border-color 0.15s;
+}
+
+.chat-input-area textarea:focus {
+ border-color: var(--accent);
+}
+
+.chat-input-area textarea::placeholder {
+ color: var(--text-muted);
+}
+
+.chat-input-area .btn {
+ border-radius: 20px;
+ padding: 8px 20px;
+ flex-shrink: 0;
+ align-self: flex-end;
+}
+
+.chat-error {
+ color: var(--danger);
+ font-size: 12px;
+ padding: 4px 0;
+ text-align: center;
+}
+
+/* Mobile */
+@media (max-width: 768px) {
+ #chat-app {
+ height: calc(100vh - 180px);
+ }
+ .chat-message {
+ max-width: 90%;
+ }
+ .chat-message .bubble {
+ font-size: 14px;
+ }
+ .chat-profile-tab {
+ font-size: 11px;
+ padding: 4px 10px;
+ }
+}
+
/* ── Config Panel ───────────────────────────────────────────────── */
.config-panel { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
.config-panel h2 { margin: 0; font-size: 16px; }