This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/src/dashboard/templates/partials/timmy_panel.html
Alexander Whitestone 2e92838033 fix: restore real-time chat responses via WebSocket (#98)
The chat WebSocket return path was broken by two bugs that prevented
Timmy's responses from appearing in the live chat feed:

1. Frontend checked msg.type instead of msg.event for 'timmy_response'
   events — the WSEvent dataclass uses 'event' as the field name.
2. Frontend accessed msg.response instead of msg.data.response — the
   response payload is nested in the data field.

Additional fixes:
- Queue acknowledgment ("Message queued...") no longer logged as an
  agent message in chat history; the real response is logged by the
  task processor when it completes, eliminating duplicate messages.
- Chat message template now carries data-task-id so the WS handler
  can find and replace the placeholder with the actual response.
- appendMessage() uses DOM APIs (textContent) instead of innerHTML
  for safer content insertion before markdown rendering.
- Fixed chat_message.html script targeting when queue-status div is
  present between the agent message and the inline script.

https://claude.ai/code/session_011cJfexqBBuGhSRQU8qwKcR

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-28 20:22:47 -05:00

205 lines
7.8 KiB
HTML

<div id="main-panel" class="col-12 col-md-9 d-flex flex-column mc-chat-panel">
<div class="card mc-panel flex-grow-1 d-flex flex-column min-h-0">
<div class="card-header mc-panel-header d-flex justify-content-between align-items-center">
<span class="d-flex align-items-center gap-2">
{% if agent %}
<span class="status-dot {{ 'green' if agent.status == 'idle' else 'amber' }}"></span>
{% endif %}
// TIMMY INTERFACE
<span id="timmy-status" class="ms-2" style="font-size: 0.75rem; color: #888;">
<span class="htmx-indicator">checking...</span>
</span>
</span>
<button class="mc-btn-clear"
hx-delete="/agents/timmy/history"
hx-target="#chat-log"
hx-swap="innerHTML"
hx-confirm="Clear conversation history?">CLEAR</button>
</div>
<div id="current-task-banner" class="current-task-banner" style="display: none; background: #1a1a2e; padding: 8px 12px; border-bottom: 1px solid #333;">
<small style="color: #00ff88;">&#9679; WORKING:</small>
<span id="current-task-title" style="color: #fff;"></span>
</div>
<div class="chat-log flex-grow-1 overflow-auto p-3" id="chat-log"
hx-get="/agents/timmy/history"
hx-trigger="load"
hx-swap="innerHTML"
hx-on::after-settle="scrollChat()"></div>
<div class="card-footer mc-chat-footer">
<form hx-post="/agents/timmy/chat"
hx-target="#chat-log"
hx-swap="beforeend"
hx-indicator="#send-indicator"
hx-sync="this:drop"
hx-disabled-elt="find button"
hx-on::after-settle="scrollChat()"
hx-on::after-request="if(event.detail.successful){this.querySelector('[name=message]').value='';}"
class="d-flex gap-2"
id="timmy-chat-form">
<input type="text"
name="message"
class="form-control mc-input"
placeholder="send a message to timmy..."
autocomplete="off"
autocorrect="off"
autocapitalize="none"
spellcheck="false"
enterkeyhint="send"
required
id="timmy-chat-input" />
<button type="submit" class="btn mc-btn-send">
SEND
<span id="send-indicator" class="htmx-indicator">&#x25FC;</span>
</button>
<button type="button"
class="btn"
style="background: #1a1a2e; color: #00ff88; border: 1px solid #00ff88;
font-size: 0.7rem; white-space: nowrap; padding: 4px 10px;"
onclick="askGrok()"
title="Send directly to Grok (xAI)">
GROK
</button>
</form>
</div>
</div>
</div>
<script>
scrollChat();
function askGrok() {
var input = document.getElementById('timmy-chat-input');
if (!input || !input.value.trim()) return;
var form = document.getElementById('timmy-chat-form');
// Temporarily redirect form to Grok endpoint
var originalAction = form.getAttribute('hx-post');
form.setAttribute('hx-post', '/grok/chat');
htmx.process(form);
htmx.trigger(form, 'submit');
// Restore original action after submission
setTimeout(function() {
form.setAttribute('hx-post', originalAction);
htmx.process(form);
}, 100);
}
// Poll for Timmy's queue status (fallback) + WebSocket for real-time
(function() {
var statusEl = document.getElementById('timmy-status');
var banner = document.getElementById('current-task-banner');
var taskTitle = document.getElementById('current-task-title');
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
var ws;
var chatLog = document.getElementById('chat-log');
function updateFromData(data) {
if (data.is_working && data.current_task) {
statusEl.innerHTML = '<span style="color: #ffaa00;">working...</span>';
banner.style.display = 'block';
taskTitle.textContent = data.current_task.title;
} else if (data.tasks_ahead > 0) {
statusEl.innerHTML = '<span style="color: #888;">queue: ' + data.tasks_ahead + ' ahead</span>';
banner.style.display = 'none';
} else {
statusEl.innerHTML = '<span style="color: #00ff88;">ready</span>';
banner.style.display = 'none';
}
}
function fetchStatus() {
fetch('/api/queue/status?assigned_to=timmy')
.then(r => r.json())
.then(updateFromData)
.catch(() => {});
}
function appendMessage(role, content, timestamp, taskId) {
// If this is a response for a queued task, replace the placeholder
if (taskId) {
var placeholder = chatLog.querySelector('.chat-message.agent[data-task-id="' + taskId + '"]');
if (placeholder) {
placeholder.removeAttribute('data-task-id');
var body = placeholder.querySelector('.msg-body');
if (body) {
body.textContent = content;
body.className = 'msg-body timmy-md';
if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
body.innerHTML = DOMPurify.sanitize(marked.parse(body.textContent));
if (typeof hljs !== 'undefined') {
body.querySelectorAll('pre code').forEach(function(block) { hljs.highlightElement(block); });
}
}
}
placeholder.querySelector('.msg-meta').textContent = 'TIMMY // ' + timestamp;
// Remove queue-status indicator if present
var qs = placeholder.nextElementSibling;
if (qs && qs.classList.contains('queue-status')) qs.remove();
chatLog.scrollTop = chatLog.scrollHeight;
return;
}
}
var div = document.createElement('div');
div.className = 'chat-message ' + role;
var meta = document.createElement('div');
meta.className = 'msg-meta';
meta.textContent = (role === 'user' ? 'YOU' : 'TIMMY') + ' // ' + timestamp;
var body = document.createElement('div');
body.className = 'msg-body timmy-md';
body.textContent = content;
div.appendChild(meta);
div.appendChild(body);
chatLog.appendChild(div);
// Render markdown if available
if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
body.innerHTML = DOMPurify.sanitize(marked.parse(body.textContent));
if (typeof hljs !== 'undefined') {
body.querySelectorAll('pre code').forEach(function(block) { hljs.highlightElement(block); });
}
}
// Scroll to bottom
chatLog.scrollTop = chatLog.scrollHeight;
}
function connectWs() {
try {
ws = new WebSocket(protocol + '//' + window.location.host + '/swarm/live');
ws.onmessage = function(event) {
try {
var msg = JSON.parse(event.data);
// Refresh on task events
if (msg.type === 'task_event') {
fetchStatus();
} else if (msg.type === 'timmy_thought') {
// Timmy thought - could show in UI
} else if (msg.event === 'task_created' || msg.event === 'task_completed' ||
msg.event === 'task_approved') {
fetchStatus();
} else if (msg.event === 'timmy_response' && msg.data) {
// Timmy pushed a response via task processor
var now = new Date();
var ts = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0') + ':' + now.getSeconds().toString().padStart(2,'0');
appendMessage('agent', msg.data.response, ts, msg.data.task_id);
fetchStatus();
}
} catch(e) {}
};
ws.onclose = function() {
setTimeout(connectWs, 5000);
};
} catch(e) {}
}
// Initial fetch + start WebSocket
fetchStatus();
connectWs();
// Also poll periodically as fallback (every 10s)
setInterval(fetchStatus, 10000);
})();
</script>