Created
April 27, 2025 18:16
-
-
Save jshmllr/fd1d5a81644ad93c802a3b5b9af4f73b to your computer and use it in GitHub Desktop.
A design prototype for T3 chat that creates a timeline functionality.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(() => { | |
// 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); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Paste this in the browser console for content injection.