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