chat.html•6.88 kB
{% extends "base.html" %}
{% block title %}{{ chat['title'] or 'Untitled Chat' }} - MCP Perplexity{% endblock %}
{% block content %}
<div class="dark:bg-tokyo-bg-secondary shadow sm:rounded-lg">
<div class="px-4 py-3 sm:px-6">
<div class="flex items-center justify-between">
<div>
<h2 class="text-md leading-6 font-medium dark:text-tokyo-accent-blue">
{{ chat['title'] or 'Untitled Chat' }}
</h2>
<p class="mt-0.5 max-w-2xl text-sm dark:text-tokyo-text">
Created {{ chat['created_at'].strftime('%Y-%m-%d %H:%M:%S') }}
</p>
</div>
<div class="flex items-center">
<span class="inline-flex items-center rounded-full dark:bg-tokyo-bg-accent px-2.5 py-0.5 text-xs font-medium dark:text-tokyo-text">
ID: {{ chat['id'] }}
</span>
<button class="copy-chat-id ml-1 dark:text-tokyo-text-secondary hover:text-tokyo-accent-blue focus:outline-none focus:ring-1 focus:ring-tokyo-accent-blue rounded-md transition-colors duration-150"
data-chat-id="{{ chat['id'] }}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2" />
<path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z" />
</svg>
</button>
<span class="copied-text ml-2 text-tokyo-accent-green text-xs hidden">Copied!</span>
<a href="/" class="ml-4 inline-flex items-center px-4 py-2 border dark:border-tokyo-bg-accent shadow-sm text-sm font-medium rounded-md dark:text-tokyo-text dark:bg-tokyo-bg-accent hover:bg-tokyo-bg-message transition-colors duration-150">
Back to Chats
</a>
</div>
</div>
</div>
<div class="border-t dark:border-tokyo-bg-accent">
<!-- Container for messages - No longer has direct HTMX update attributes -->
<div class="chat-container px-4 py-3 sm:px-6">
{% include '_message_list.html' %}
</div>
</div>
</div>
<!-- Hidden div that acts as a data fetcher and updates messages only if there are changes -->
<div id="message-updater"
hx-get="/api/chat/{{ chat['id'] }}/messages"
hx-trigger="every 5s"
hx-target="#message-updater"
hx-swap="innerHTML"
class="hidden">
</div>
<script>
let lastMessageCount = document.querySelectorAll('.message').length;
let messageData = null;
// Scroll to bottom only on initial load
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('.chat-container');
container.scrollTop = container.scrollHeight;
// Initialize the message data on load
messageData = Array.from(document.querySelectorAll('.message')).map(el => el.id);
});
// On HTMX content update to the hidden updater
document.body.addEventListener('htmx:afterSwap', (event) => {
if (event.detail.target.id !== 'message-updater') return;
// Parse the new data and compare with current state
const updatedMessages = Array.from(event.detail.target.querySelectorAll('.message'));
const updatedMessageIds = updatedMessages.map(el => el.id);
// Only update if there are actual changes in the messages
if (JSON.stringify(updatedMessageIds) !== JSON.stringify(messageData)) {
// Save scroll position and details state
const container = document.querySelector('.chat-container');
const scrollPosition = container.scrollTop;
const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 100;
// Save states of any open details elements
const openDetailsState = {};
document.querySelectorAll('.chat-container details').forEach(detail => {
openDetailsState[detail.querySelector('summary').textContent.trim()] = detail.open;
});
// Update the messages container with new content
document.querySelector('.chat-container').innerHTML = event.detail.target.innerHTML;
// Restore open state of details elements
document.querySelectorAll('.chat-container details').forEach(detail => {
const summary = detail.querySelector('summary').textContent.trim();
if (openDetailsState[summary]) {
detail.open = true;
}
});
// Update our tracked message data
messageData = updatedMessageIds;
// Restore scroll position or scroll to bottom for new messages
if (isNearBottom) {
container.scrollTop = container.scrollHeight;
} else {
container.scrollTop = scrollPosition;
}
}
});
// Copy chat ID functionality
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.copy-chat-id').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const chatId = this.getAttribute('data-chat-id');
navigator.clipboard.writeText(chatId).then(() => {
// Visual feedback: Show "Copied!" text
let sibling = this.nextElementSibling;
while (sibling && !sibling.classList.contains('copied-text')) {
sibling = sibling.nextElementSibling;
}
if (sibling) {
sibling.classList.remove('hidden');
setTimeout(() => {
sibling.classList.add('hidden');
}, 1500);
}
// Visual feedback - temporarily change the icon color
const svg = this.querySelector('svg');
svg.className.baseVal = 'h-4 w-4 text-tokyo-accent-green';
setTimeout(() => {
svg.className.baseVal = 'h-4 w-4';
}, 1000);
}).catch(err => {
console.error('Failed to copy:', err);
});
});
});
});
</script>
{% endblock %}