blob: 0e535866c8028a509bec5af976e7d488b837d07a [file] [log] [blame]
bankseand52d39d2025-07-20 14:57:38 -07001/* eslint-disable @typescript-eslint/no-explicit-any */
2import { DemoModule } from "./demo-framework/types";
3import { demoUtils } from "./demo-fixtures/index";
4
5const demo: DemoModule = {
6 title: "Sketch Timeline Viewport Demo",
7 description:
8 "Timeline viewport rendering with memory leak protection and event-driven approach",
9 imports: ["../sketch-timeline.ts"],
10
11 customStyles: `
12 .demo-container {
13 max-width: 800px;
14 margin: 20px auto;
15 background: white;
16 border-radius: 8px;
17 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
18 height: 600px;
19 display: flex;
20 flex-direction: column;
21 }
22 .demo-header {
23 padding: 20px;
24 border-bottom: 1px solid #eee;
25 background: #f8f9fa;
26 border-radius: 8px 8px 0 0;
27 }
28 .demo-timeline {
29 flex: 1;
30 overflow: hidden;
31 }
32 .controls {
33 padding: 10px 20px;
34 border-top: 1px solid #eee;
35 background: #f8f9fa;
36 display: flex;
37 gap: 10px;
38 align-items: center;
39 flex-wrap: wrap;
40 }
41 button {
42 padding: 8px 16px;
43 border: 1px solid #ddd;
44 border-radius: 4px;
45 background: white;
46 cursor: pointer;
47 }
48 button:hover {
49 background: #f0f0f0;
50 }
51 .info {
52 font-size: 12px;
53 color: #666;
54 margin-left: auto;
55 }
56 `,
57
58 setup: async (container: HTMLElement) => {
59 const section = demoUtils.createDemoSection(
60 "Timeline Viewport Rendering",
61 "Demonstrates viewport-based rendering where only visible messages are rendered. Includes tests for memory leak fixes and race conditions.",
62 );
63
64 // Create demo container
65 const demoContainer = document.createElement("div");
66 demoContainer.className = "demo-container";
67
68 // Create header
69 const demoHeader = document.createElement("div");
70 demoHeader.className = "demo-header";
71 demoHeader.innerHTML = `
72 <h1>Sketch Timeline Viewport Rendering Demo</h1>
73 <p>
74 This demo shows how the timeline only renders messages in the
75 viewport. Only the most recent N messages are rendered initially, with
76 older messages loaded on scroll.
77 </p>
78 `;
79
80 // Create timeline container
81 const demoTimeline = document.createElement("div");
82 demoTimeline.className = "demo-timeline";
83
84 // Create the timeline component
85 const timeline = document.createElement("sketch-timeline") as any;
86 timeline.id = "timeline";
87 timeline.initialMessageCount = 20;
88 timeline.loadChunkSize = 10;
89
90 demoTimeline.appendChild(timeline);
91
92 // Create controls
93 const controls = document.createElement("div");
94 controls.className = "controls";
95
96 const info = document.createElement("span");
97 info.className = "info";
98 info.id = "info";
99 info.textContent = "Ready";
100
101 // Helper functions
102 const setupScrollContainer = () => {
103 if (timeline.shadowRoot) {
104 const scrollContainer =
105 timeline.shadowRoot.querySelector("#scroll-container");
106 if (scrollContainer) {
107 timeline.scrollContainer = { value: scrollContainer };
108 console.log("Scroll container set up:", scrollContainer);
109 return true;
110 }
111 }
112 return false;
113 };
114
115 const waitForShadowDOM = () => {
116 if (setupScrollContainer()) {
117 return;
118 }
119
120 const observer = new MutationObserver(() => {
121 if (timeline.shadowRoot) {
122 observer.disconnect();
123 timeline.updateComplete.then(() => {
124 setupScrollContainer();
125 });
126 }
127 });
128
129 observer.observe(timeline, { childList: true, subtree: true });
130
131 timeline.updateComplete.then(() => {
132 if (!timeline.scrollContainer || !timeline.scrollContainer.value) {
133 setupScrollContainer();
134 }
135 });
136 };
137
138 const generateMessages = (count: number) => {
139 const messages = [];
140 for (let i = 0; i < count; i++) {
141 messages.push({
142 type: i % 3 === 0 ? "user" : "agent",
143 end_of_turn: true,
144 content: `Message ${i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.`,
145 timestamp: new Date(Date.now() - (count - i) * 60000).toISOString(),
146 conversation_id: "demo-conversation",
147 idx: i,
148 });
149 }
150
151 timeline.messages = messages;
152 timeline.resetViewport();
153
154 timeline.updateComplete.then(() => {
155 const showing = Math.min(count, timeline.initialMessageCount);
156 const expectedFirst = Math.max(1, count - showing + 1);
157 const expectedLast = count;
158 info.textContent = `${count} total messages, showing most recent ${showing} (messages ${expectedFirst}-${expectedLast})`;
159
160 if (!timeline.scrollContainer || !timeline.scrollContainer.value) {
161 setupScrollContainer();
162 }
163 });
164 };
165
166 // Create control buttons
167 const btn50 = demoUtils.createButton("50 Messages", () =>
168 generateMessages(50),
169 );
170 const btn100 = demoUtils.createButton("100 Messages", () =>
171 generateMessages(100),
172 );
173 const btn500 = demoUtils.createButton("500 Messages", () =>
174 generateMessages(500),
175 );
176 const btnClear = demoUtils.createButton("Clear", () => {
177 timeline.messages = [];
178 timeline.updateComplete.then(() => {
179 info.textContent = "Messages cleared";
180 });
181 });
182 const btnReset = demoUtils.createButton("Reset Viewport", () => {
183 timeline.resetViewport();
184 info.textContent = "Viewport reset to most recent messages";
185 });
186 const btnMemoryTest = demoUtils.createButton("Test Memory Leak Fix", () => {
187 let cleanupCount = 0;
188 const originalRemoveEventListener =
189 HTMLElement.prototype.removeEventListener;
190 HTMLElement.prototype.removeEventListener = function (
191 type: string,
192 listener: any,
193 ) {
194 if (type === "scroll") {
195 cleanupCount++;
196 console.log("Scroll event listener removed");
197 }
198 return originalRemoveEventListener.call(this, type, listener);
199 };
200
201 const mockContainer1 = document.createElement("div");
202 const mockContainer2 = document.createElement("div");
203
204 timeline.scrollContainer = { value: mockContainer1 };
205 timeline.scrollContainer = { value: mockContainer2 };
206 timeline.scrollContainer = { value: null };
207 timeline.scrollContainer = { value: mockContainer1 };
208
209 if (timeline.removeScrollListener) {
210 timeline.removeScrollListener();
211 }
212
213 HTMLElement.prototype.removeEventListener = originalRemoveEventListener;
214 info.textContent = `Memory leak fix test completed. Cleanup calls: ${cleanupCount}`;
215 });
216
217 controls.appendChild(btn50);
218 controls.appendChild(btn100);
219 controls.appendChild(btn500);
220 controls.appendChild(btnClear);
221 controls.appendChild(btnReset);
222 controls.appendChild(btnMemoryTest);
223 controls.appendChild(info);
224
225 // Assemble the demo
226 demoContainer.appendChild(demoHeader);
227 demoContainer.appendChild(demoTimeline);
228 demoContainer.appendChild(controls);
229
230 section.appendChild(demoContainer);
231 container.appendChild(section);
232
233 // Initialize
234 waitForShadowDOM();
235
236 // Generate initial messages after a brief delay
237 setTimeout(() => {
238 generateMessages(100);
239 }, 100);
240 },
241};
242
243export default demo;