Skip to content

Instantly share code, notes, and snippets.

@jshmllr
Created April 27, 2025 18:16
Show Gist options
  • Save jshmllr/fd1d5a81644ad93c802a3b5b9af4f73b to your computer and use it in GitHub Desktop.
Save jshmllr/fd1d5a81644ad93c802a3b5b9af4f73b to your computer and use it in GitHub Desktop.
A design prototype for T3 chat that creates a timeline functionality.
(() => {
// Find the chat log container
const chatLog = document.querySelector('div[role="log"][aria-label="Chat messages"]');
if (!chatLog) {
console.warn('Chat container not found.');
return;
}
// Remove existing timeline if any
const existingTimeline = document.getElementById('chat-timeline-sidebar');
if (existingTimeline) existingTimeline.remove();
// Move all messages inside a wrapper
const messageWrapper = document.createElement('div');
messageWrapper.id = 'chat-messages-wrapper';
messageWrapper.style.cssText = `
flex: 8;
overflow-y: auto;
position: relative;
transition: flex 0.3s ease;
will-change: flex; /* Performance hint for the browser */
`;
// Move all messages into the wrapper
while (chatLog.firstChild) {
messageWrapper.appendChild(chatLog.firstChild);
}
chatLog.appendChild(messageWrapper);
// Create sidebar container
const sidebar = document.createElement('div');
sidebar.id = 'chat-timeline-sidebar';
sidebar.style.cssText = `
flex: 2;
min-width: 20%;
max-width: 20%;
max-height: 100%;
overflow-y: auto;
font-family: system-ui, sans-serif;
font-size: 12px;
padding: 0px 8px 10px 8px;
box-sizing: border-box;
user-select: none;
transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1); /* Improved easing */
transform-origin: right center;
border-left: none;
margin-top: 0px;
will-change: transform, opacity, min-width, max-width; /* Performance hint */
`;
// Title
const titleColor = '#4F1754';
const title = document.createElement('h4');
title.textContent = 'Timeline';
title.style.cssText = `
margin: 0 0 8px 0;
font-weight: 600;
font-size: 14px;
text-align: left;
color: ${titleColor};
`;
sidebar.appendChild(title);
// List
const list = document.createElement('div');
list.id = 'chat-timeline-list';
list.style.cssText = `
display: flex;
flex-direction: column;
gap: 4px;
`;
sidebar.appendChild(list);
// Set up chat log as a flex container
chatLog.style.display = 'flex';
chatLog.style.flexDirection = 'row';
chatLog.style.height = '100%';
chatLog.style.position = 'relative';
chatLog.appendChild(sidebar);
let activeBtn = null;
function setActive(btn) {
if (activeBtn) {
activeBtn.style.color = '#A80063';
activeBtn.style.fontWeight = 'normal';
activeBtn.textContent = activeBtn.textContent.replace(/^— /, '');
}
activeBtn = btn;
activeBtn.style.color = '#4F1754';
activeBtn.style.fontWeight = 'bold';
activeBtn.textContent = '— ' + activeBtn.textContent;
}
function formatTime(date) {
return date.toLocaleTimeString([], { hour12: false });
}
const messages = Array.from(messageWrapper.children).filter(
(el) => el.hasAttribute && el.hasAttribute('data-message-id')
);
if (messages.length === 0) {
list.textContent = 'No messages.';
list.style.textAlign = 'center';
list.style.color = '#ccc';
return;
}
const now = new Date();
const timestamps = messages.map((_, i) => {
const d = new Date(now.getTime() + i * 60000);
return formatTime(d);
});
timestamps.forEach((ts, i) => {
const btn = document.createElement('button');
btn.textContent = ts;
btn.title = `Go to message #${i + 1}`;
btn.style.cssText = `
background: transparent;
border: none;
color: #A80063;
text-align: left;
cursor: pointer;
padding: 3px 0px;
border-radius: 3px;
transition: background-color 0.2s, color 0.2s;
font-variant-numeric: tabular-nums;
width: 100%;
`;
btn.onmouseenter = () => (btn.style.backgroundColor = '#EFCAE4');
btn.onmouseleave = () => (btn.style.backgroundColor = 'transparent');
btn.onclick = () => {
messages[i].scrollIntoView({ behavior: 'smooth', block: 'center' });
setActive(btn);
};
list.appendChild(btn);
});
// Find or create the top-right container for buttons
let headerContainer = document.querySelector('.fixed.right-2.top-2.z-20');
let innerFlexContainer;
if (!headerContainer) {
// Create the container if it doesn't exist
headerContainer = document.createElement('div');
headerContainer.className = 'fixed right-2 top-2 z-20 max-sm:hidden';
headerContainer.style.cssText = 'right: var(--firefox-scrollbar, 0.5rem);';
document.body.appendChild(headerContainer);
// Create the inner flex container
innerFlexContainer = document.createElement('div');
innerFlexContainer.className = 'flex flex-row items-center text-muted-foreground gap-0.5 rounded-md p-1 transition-all blur-fallback:bg-sidebar bg-sidebar/50 backdrop-blur-sm';
headerContainer.appendChild(innerFlexContainer);
} else {
// Use existing inner flex container
innerFlexContainer = headerContainer.querySelector('.flex.flex-row');
}
// Create the timeline toggle button
const timelineToggleButton = document.createElement('button');
timelineToggleButton.className = 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 disabled:cursor-not-allowed hover:bg-muted/40 hover:text-foreground disabled:hover:bg-transparent disabled:hover:text-foreground/50 group relative size-8';
timelineToggleButton.setAttribute('data-state', 'open');
timelineToggleButton.setAttribute('aria-label', 'Toggle timeline');
// Create the left (collapse) arrow SVG
const leftArrowSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
leftArrowSVG.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
leftArrowSVG.setAttribute('width', '24');
leftArrowSVG.setAttribute('height', '24');
leftArrowSVG.setAttribute('viewBox', '0 0 24 24');
leftArrowSVG.setAttribute('fill', 'none');
leftArrowSVG.setAttribute('stroke', 'currentColor');
leftArrowSVG.setAttribute('stroke-width', '2');
leftArrowSVG.setAttribute('stroke-linecap', 'round');
leftArrowSVG.setAttribute('stroke-linejoin', 'round');
leftArrowSVG.classList.add('absolute', 'size-4', 'rotate-90', 'scale-0', 'transition-all', 'duration-200');
// Add the path for left arrow
const leftArrowPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
leftArrowPath.setAttribute('d', 'M15 18l-6-6 6-6');
leftArrowSVG.appendChild(leftArrowPath);
// Create the right (expand) arrow SVG
const rightArrowSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
rightArrowSVG.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
rightArrowSVG.setAttribute('width', '24');
rightArrowSVG.setAttribute('height', '24');
rightArrowSVG.setAttribute('viewBox', '0 0 24 24');
rightArrowSVG.setAttribute('fill', 'none');
rightArrowSVG.setAttribute('stroke', 'currentColor');
rightArrowSVG.setAttribute('stroke-width', '2');
rightArrowSVG.setAttribute('stroke-linecap', 'round');
rightArrowSVG.setAttribute('stroke-linejoin', 'round');
rightArrowSVG.classList.add('absolute', 'size-4', 'rotate-0', 'scale-100', 'transition-all', 'duration-200');
// Add the path for right arrow
const rightArrowPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
rightArrowPath.setAttribute('d', 'M9 18l6-6-6-6');
rightArrowSVG.appendChild(rightArrowPath);
// Create the timeline icon
const timelineIconSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
timelineIconSVG.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
timelineIconSVG.setAttribute('width', '24');
timelineIconSVG.setAttribute('height', '24');
timelineIconSVG.setAttribute('viewBox', '0 0 24 24');
timelineIconSVG.setAttribute('fill', 'none');
timelineIconSVG.setAttribute('stroke', 'currentColor');
timelineIconSVG.setAttribute('stroke-width', '2');
timelineIconSVG.setAttribute('stroke-linecap', 'round');
timelineIconSVG.setAttribute('stroke-linejoin', 'round');
timelineIconSVG.classList.add('absolute', 'size-4', 'rotate-0', 'scale-0', 'transition-all', 'duration-200');
// Add the paths for timeline icon (simplified clock/history icon)
const timelineCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
timelineCircle.setAttribute('cx', '12');
timelineCircle.setAttribute('cy', '12');
timelineCircle.setAttribute('r', '10');
timelineIconSVG.appendChild(timelineCircle);
const timelineLine1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
timelineLine1.setAttribute('x1', '12');
timelineLine1.setAttribute('y1', '12');
timelineLine1.setAttribute('x2', '12');
timelineLine1.setAttribute('y2', '8');
timelineIconSVG.appendChild(timelineLine1);
const timelineLine2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
timelineLine2.setAttribute('x1', '12');
timelineLine2.setAttribute('y1', '12');
timelineLine2.setAttribute('x2', '16');
timelineLine2.setAttribute('y2', '12');
timelineIconSVG.appendChild(timelineLine2);
// Add screen reader text
const srOnlySpan = document.createElement('span');
srOnlySpan.textContent = 'Toggle timeline';
srOnlySpan.className = 'sr-only';
// Add elements to button
timelineToggleButton.appendChild(leftArrowSVG);
timelineToggleButton.appendChild(rightArrowSVG);
timelineToggleButton.appendChild(timelineIconSVG);
timelineToggleButton.appendChild(srOnlySpan);
// Add button to the container
innerFlexContainer.appendChild(timelineToggleButton);
let isCollapsed = false;
timelineToggleButton.onclick = () => {
isCollapsed = !isCollapsed;
// Update button state attribute
timelineToggleButton.setAttribute('data-state', isCollapsed ? 'closed' : 'open');
if (isCollapsed) {
// When collapsing
sidebar.style.minWidth = '0';
sidebar.style.maxWidth = '0';
sidebar.style.padding = '0';
sidebar.style.opacity = '0';
sidebar.style.overflow = 'hidden';
sidebar.style.transform = 'translateX(20px)'; // Slide out effect
// Make message wrapper take up full width
messageWrapper.style.flex = '1';
// Update button appearance - show left arrow (to expand)
leftArrowSVG.classList.remove('rotate-90', 'scale-0');
leftArrowSVG.classList.add('rotate-0', 'scale-100');
rightArrowSVG.classList.remove('rotate-0', 'scale-100');
rightArrowSVG.classList.add('rotate-90', 'scale-0');
timelineIconSVG.classList.remove('scale-0');
timelineIconSVG.classList.add('scale-100');
} else {
// When expanding
sidebar.style.minWidth = '20%';
sidebar.style.maxWidth = '20%';
sidebar.style.padding = '0px 8px 10px 8px';
sidebar.style.opacity = '1';
sidebar.style.overflow = 'auto';
sidebar.style.transform = 'translateX(0)'; // Slide in effect
// Restore message wrapper size
messageWrapper.style.flex = '8';
// Update button appearance - show right arrow (to collapse)
leftArrowSVG.classList.remove('rotate-0', 'scale-100');
leftArrowSVG.classList.add('rotate-90', 'scale-0');
rightArrowSVG.classList.remove('rotate-90', 'scale-0');
rightArrowSVG.classList.add('rotate-0', 'scale-100');
timelineIconSVG.classList.remove('scale-100');
timelineIconSVG.classList.add('scale-0');
}
};
// Add intersection observer for better scroll performance
const messageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const messageIndex = messages.indexOf(entry.target);
if (messageIndex >= 0 && list.children[messageIndex]) {
setActive(list.children[messageIndex]);
}
}
});
}, { threshold: 0.5 });
// Observe all messages
messages.forEach(message => {
messageObserver.observe(message);
});
// Optional: style scrollbar for sidebar
const style = document.createElement('style');
style.textContent = `
#chat-timeline-sidebar::-webkit-scrollbar {
width: 6px;
}
#chat-timeline-sidebar::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
#chat-timeline-sidebar::-webkit-scrollbar-track {
background: transparent;
}
#chat-timeline-sidebar::-webkit-scrollbar-corner {
background: transparent;
}
`;
document.head.appendChild(style);
})();
@jshmllr
Copy link
Author

jshmllr commented Apr 27, 2025

Paste this in the browser console for content injection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment