Following discussions in various issues (
Add pagination to ChatFeed · Issue #6021 · holoviz/panel · GitHub) about listing a long list of messages while keeping usage of resources low ; and being able to jump to any message “quickly”, I came up with this reactiveHTML component. It works with tens of thousand of messages.
Perhaps useful to others. I can’t use Panel’s ChatMessage due to the generation of the messages on DOM side on the fly based on a given dictionary, so I came up with an own format.
import param
import datetime
import panel as pn
from panel.reactive import ReactiveHTML
class MessageViewer(ReactiveHTML):
messages = param.List(default=[]) # List of message dictionaries
message_index = param.Integer(default=None) # Index to scroll to
_template = """
<div id="message-container" style="overflow-y: auto; height: 90vh; background: #1f77b4;">
{% for message in messages %}
<div class="message" style="height: 50px;" id="message-{{ loop.index0 }}" data-index="{{ loop.index0 }}">
</div>
{% endfor %}
</div>
"""
_scripts = {
'message_index': """
if (data.message_index !== null) {
Array.from(message_container.childNodes).forEach(child => {
if (child.classList && child.classList.contains('message') && (child.getAttribute('data-index')==data.message_index)) {
console.log(child);
child.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
});
};
""",
'after_layout': """
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const messageElement = entry.target;
const index = messageElement.getAttribute('data-index');
const messageData = data.messages[index];
if (entry.isIntersecting) {
messageElement.innerHTML = `
<div class='inner-message' style="padding: 10px; display: flex; justify-content: space-between; align-items: center; background: white; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
<div>
<p style="margin: 0; font-weight: 500;">${messageData.text}</p>
<small style="color: gray;">${new Date(messageData.timestamp * 1000).toLocaleString()}</small>
</div>
<div style="display: flex; align-items: center;">
<div style="margin-right: 16px;">
${messageData.tags.map(tag => `<span style="background: #e0e0e0; border-radius: 16px; padding: 4px 8px; margin-right: 4px; font-size: 0.8em;">${tag}</span>`).join('')}
</div>
<small style="color: gray;">#${messageData.index}</small>
</div>
</div>
`;
} else {
// Clear the innerHTML when the message is out of view
messageElement.innerHTML = '';
}
});
}, { rootMargin: '50px' });
Array.from(message_container.childNodes).forEach(child => {
if (child.classList && child.classList.contains('message')) {
observer.observe(child);
}
});
self.message_index();
"""
}
def __init__(self, **params):
super().__init__(**params)
# Example usage
messages = [
{'text': f'Message {i}', 'timestamp': 123456789 + i*360, 'tags': [f'tag{i%3}'], 'index': i}
for i in range(1000)
]
message_viewer = MessageViewer(messages=messages, message_index=0, sizing_mode='stretch_both')
try:
s.stop()
except:
pass
s = message_viewer.show(port = 5006, open=False)
Then you can jump to any message id with message_viewer.message_index = 30