blob: e8d4a8fafe897ab66ac3ae254a0bcee712704023 [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;
banksean1ee0bc62025-07-22 23:24:18 +000024 border-bottom: 1px solid var(--demo-border);
25 background: var(--demo-fixture-section-bg);
bankseand52d39d2025-07-20 14:57:38 -070026 border-radius: 8px 8px 0 0;
27 }
28 .demo-timeline {
29 flex: 1;
30 overflow: hidden;
31 }
32 .controls {
33 padding: 10px 20px;
banksean1ee0bc62025-07-22 23:24:18 +000034 border-top: 1px solid var(--demo-border);
35 background: var(--demo-fixture-section-bg);
bankseand52d39d2025-07-20 14:57:38 -070036 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;
banksean1ee0bc62025-07-22 23:24:18 +000053 color: var(--demo-secondary-text);
bankseand52d39d2025-07-20 14:57:38 -070054 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 = () => {
banksean44320562025-07-21 11:09:38 -0700103 const scrollContainer = timeline.querySelector("#scroll-container");
104 if (scrollContainer) {
105 timeline.scrollContainer = { value: scrollContainer };
106 console.log("Scroll container set up:", scrollContainer);
107 return true;
bankseand52d39d2025-07-20 14:57:38 -0700108 }
109 return false;
110 };
111
112 const waitForShadowDOM = () => {
113 if (setupScrollContainer()) {
114 return;
115 }
116
117 const observer = new MutationObserver(() => {
banksean44320562025-07-21 11:09:38 -0700118 observer.disconnect();
119 timeline.updateComplete.then(() => {
120 setupScrollContainer();
121 });
bankseand52d39d2025-07-20 14:57:38 -0700122 });
123
124 observer.observe(timeline, { childList: true, subtree: true });
125
126 timeline.updateComplete.then(() => {
127 if (!timeline.scrollContainer || !timeline.scrollContainer.value) {
128 setupScrollContainer();
129 }
130 });
131 };
132
133 const generateMessages = (count: number) => {
134 const messages = [];
135 for (let i = 0; i < count; i++) {
136 messages.push({
137 type: i % 3 === 0 ? "user" : "agent",
138 end_of_turn: true,
139 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.`,
140 timestamp: new Date(Date.now() - (count - i) * 60000).toISOString(),
141 conversation_id: "demo-conversation",
142 idx: i,
143 });
144 }
145
146 timeline.messages = messages;
147 timeline.resetViewport();
148
149 timeline.updateComplete.then(() => {
150 const showing = Math.min(count, timeline.initialMessageCount);
151 const expectedFirst = Math.max(1, count - showing + 1);
152 const expectedLast = count;
153 info.textContent = `${count} total messages, showing most recent ${showing} (messages ${expectedFirst}-${expectedLast})`;
154
155 if (!timeline.scrollContainer || !timeline.scrollContainer.value) {
156 setupScrollContainer();
157 }
158 });
159 };
160
161 // Create control buttons
162 const btn50 = demoUtils.createButton("50 Messages", () =>
163 generateMessages(50),
164 );
165 const btn100 = demoUtils.createButton("100 Messages", () =>
166 generateMessages(100),
167 );
168 const btn500 = demoUtils.createButton("500 Messages", () =>
169 generateMessages(500),
170 );
171 const btnClear = demoUtils.createButton("Clear", () => {
172 timeline.messages = [];
173 timeline.updateComplete.then(() => {
174 info.textContent = "Messages cleared";
175 });
176 });
177 const btnReset = demoUtils.createButton("Reset Viewport", () => {
178 timeline.resetViewport();
179 info.textContent = "Viewport reset to most recent messages";
180 });
181 const btnMemoryTest = demoUtils.createButton("Test Memory Leak Fix", () => {
182 let cleanupCount = 0;
183 const originalRemoveEventListener =
184 HTMLElement.prototype.removeEventListener;
185 HTMLElement.prototype.removeEventListener = function (
186 type: string,
187 listener: any,
188 ) {
189 if (type === "scroll") {
190 cleanupCount++;
191 console.log("Scroll event listener removed");
192 }
193 return originalRemoveEventListener.call(this, type, listener);
194 };
195
196 const mockContainer1 = document.createElement("div");
197 const mockContainer2 = document.createElement("div");
198
199 timeline.scrollContainer = { value: mockContainer1 };
200 timeline.scrollContainer = { value: mockContainer2 };
201 timeline.scrollContainer = { value: null };
202 timeline.scrollContainer = { value: mockContainer1 };
203
204 if (timeline.removeScrollListener) {
205 timeline.removeScrollListener();
206 }
207
208 HTMLElement.prototype.removeEventListener = originalRemoveEventListener;
209 info.textContent = `Memory leak fix test completed. Cleanup calls: ${cleanupCount}`;
210 });
211
212 controls.appendChild(btn50);
213 controls.appendChild(btn100);
214 controls.appendChild(btn500);
215 controls.appendChild(btnClear);
216 controls.appendChild(btnReset);
217 controls.appendChild(btnMemoryTest);
218 controls.appendChild(info);
219
220 // Assemble the demo
221 demoContainer.appendChild(demoHeader);
222 demoContainer.appendChild(demoTimeline);
223 demoContainer.appendChild(controls);
224
225 section.appendChild(demoContainer);
226 container.appendChild(section);
227
228 // Initialize
229 waitForShadowDOM();
230
231 // Generate initial messages after a brief delay
232 setTimeout(() => {
233 generateMessages(100);
234 }, 100);
235 },
236};
237
238export default demo;