thibaud frere commited on
Commit
78ea91c
·
1 Parent(s): da61e6b

update banner

Browse files
Files changed (1) hide show
  1. app/src/content/embeds/banner.html +104 -29
app/src/content/embeds/banner.html CHANGED
@@ -57,6 +57,64 @@
57
  container.style.position = container.style.position || 'relative';
58
  let tip = container.querySelector('.d3-tooltip');
59
  let tipInner;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  if (!tip) {
61
  tip = document.createElement('div');
62
  tip.className = 'd3-tooltip';
@@ -82,6 +140,8 @@
82
  tipInner.style.textAlign = 'left';
83
  tip.appendChild(tipInner);
84
  container.appendChild(tip);
 
 
85
  } else {
86
  tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
87
  }
@@ -109,6 +169,25 @@
109
  });
110
  const firstId = data && data.length ? data[0].original_id : '0';
111
  const thumbBase = await resolveBase(CANDIDATE_BASES, `${firstId}.jpg`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  // Centroides par catégorie (moyenne x/y)
114
  const centroids = Array.from(
@@ -260,47 +339,34 @@
260
  .attr('stroke', (d) => pointStroke(d))
261
  .attr('stroke-width', 0.4)
262
  .on('mouseenter', function(ev, d) {
 
263
  d3.select(this).raise()
264
  .attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
265
  .attr('stroke-width', 1.2);
266
- const imgSrc = `${thumbBase}${d.original_id}.jpg`;
267
-
268
- // Function to truncate text to max 25 words
269
- const truncateText = (text, maxWords = 25) => {
270
- if (!text || typeof text !== 'string') return '—';
271
- const words = text.trim().split(/\s+/);
272
- if (words.length <= maxWords) return text;
273
- return words.slice(0, maxWords).join(' ') + '...';
274
- };
275
-
276
- const userText = truncateText(d.user);
277
- const assistantText = truncateText(d.assistant);
278
-
279
- tipInner.innerHTML =
280
- `<div style="display:flex; gap:10px; align-items:flex-start;">` +
281
- `<div style="width:120px;height:120px;flex:0 0 auto;border-radius:6px;border:1px solid var(--border-color);display:flex;align-items:center;justify-content:center;overflow:hidden;background:var(--surface-bg);">` +
282
- `<img src="${imgSrc}" alt="thumb ${d.original_id}" style="max-width:100%;max-height:100%;object-fit:contain;display:block;" />` +
283
- `</div>` +
284
- `<div style="min-width:140px; max-width:200px;">` +
285
- `<div><strong>${d.category || 'Unknown'}</strong></div>` +
286
- `<div style="word-wrap:break-word; line-height:1.3; margin:4px 0;"><strong>Q:</strong> ${userText}</div>` +
287
- `<div style="word-wrap:break-word; line-height:1.3; margin:4px 0;"><strong>A:</strong> ${assistantText}</div>` +
288
- `<div><strong>Subset</strong> ${d.subset ?? '—'}</div>` +
289
- `</div>` +
290
- `</div>`;
291
  tip.style.opacity = '1';
 
 
292
  })
293
  .on('mousemove', (ev) => {
 
294
  const [mx, my] = d3.pointer(ev, container);
295
- const offsetX = 10, offsetY = 12;
296
- tip.style.transform = `translate(${Math.round(mx + offsetX)}px, ${Math.round(my + offsetY)}px)`;
297
  })
298
  .on('mouseleave', function() {
 
299
  tip.style.opacity = '0';
300
  tip.style.transform = 'translate(-9999px, -9999px)';
301
  d3.select(this)
302
  .attr('stroke', (d) => pointStroke(d))
303
  .attr('stroke-width', 0.4);
 
 
 
 
 
 
 
304
  }),
305
  (update) => update
306
  .attr('cx', (d) => xScale(d.x_position))
@@ -311,13 +377,20 @@
311
  .attr('stroke', (d) => pointStroke(d))
312
  .attr('stroke-width', 0.4)
313
  );
 
 
 
314
  };
315
 
316
  if (window.ResizeObserver) {
317
- const ro = new ResizeObserver(() => render());
 
 
 
 
318
  ro.observe(container);
319
  } else {
320
- window.addEventListener('resize', render);
321
  }
322
  // Re-render on theme changes (data-theme attribute updates live)
323
  const themeObserver = new MutationObserver((mutations) => {
@@ -330,6 +403,8 @@
330
  });
331
  themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
332
  render();
 
 
333
  });
334
  };
335
 
 
57
  container.style.position = container.style.position || 'relative';
58
  let tip = container.querySelector('.d3-tooltip');
59
  let tipInner;
60
+ const isMobileLayout = () => window.matchMedia('(max-width: 480px)').matches;
61
+ let tipVisible = false;
62
+ const positionTooltipWithinBounds = (mx, my) => {
63
+ if (!tip || isMobileLayout()) return;
64
+ const offsetX = 10, offsetY = 12;
65
+ const desiredX = Math.round(mx + offsetX);
66
+ const desiredY = Math.round(my + offsetY);
67
+ const tipWidth = tip.offsetWidth || 0;
68
+ const tipHeight = tip.offsetHeight || 0;
69
+ const maxX = Math.max(0, (container.clientWidth || 0) - tipWidth - 1);
70
+ const maxY = Math.max(0, (container.clientHeight || 0) - tipHeight - 1);
71
+ const clampedX = Math.max(0, Math.min(desiredX, maxX));
72
+ const clampedY = Math.max(0, Math.min(desiredY, maxY));
73
+ tip.style.transform = `translate(${clampedX}px, ${clampedY}px)`;
74
+ };
75
+ const applyTooltipLayout = () => {
76
+ if (!tip) return;
77
+ if (isMobileLayout()) {
78
+ Object.assign(tip.style, {
79
+ position: 'static',
80
+ transform: 'none',
81
+ margin: '12px auto 0',
82
+ maxWidth: 'min(420px, 92%)',
83
+ width: 'auto',
84
+ left: '0px',
85
+ top: '0px',
86
+ pointerEvents: 'auto'
87
+ });
88
+ tip.style.display = tipVisible ? 'block' : 'none';
89
+ tip.style.opacity = tipVisible ? '1' : '0';
90
+ } else {
91
+ Object.assign(tip.style, {
92
+ position: 'absolute',
93
+ opacity: '0',
94
+ transform: 'translate(-9999px, -9999px)',
95
+ margin: '0',
96
+ maxWidth: '',
97
+ width: '',
98
+ left: '0px',
99
+ top: '0px',
100
+ pointerEvents: 'none'
101
+ });
102
+ tip.style.display = 'block';
103
+ }
104
+ };
105
+ const applyContainerLayout = () => {
106
+ if (!container || !container.style) return;
107
+ container.style.flexDirection = isMobileLayout() ? 'column' : 'row';
108
+ // Optionally add a small gap for breathing space when stacked
109
+ container.style.gap = isMobileLayout() ? '10px' : '0px';
110
+ container.style.justifyContent = isMobileLayout() ? 'flex-start' : 'center';
111
+ };
112
+ const truncateText = (text, maxWords = 25) => {
113
+ if (!text || typeof text !== 'string') return '—';
114
+ const words = text.trim().split(/\s+/);
115
+ if (words.length <= maxWords) return text;
116
+ return words.slice(0, maxWords).join(' ') + '...';
117
+ };
118
  if (!tip) {
119
  tip = document.createElement('div');
120
  tip.className = 'd3-tooltip';
 
140
  tipInner.style.textAlign = 'left';
141
  tip.appendChild(tipInner);
142
  container.appendChild(tip);
143
+ applyTooltipLayout();
144
+ applyContainerLayout();
145
  } else {
146
  tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
147
  }
 
169
  });
170
  const firstId = data && data.length ? data[0].original_id : '0';
171
  const thumbBase = await resolveBase(CANDIDATE_BASES, `${firstId}.jpg`);
172
+ const updateTooltipContent = (d) => {
173
+ if (!d) return;
174
+ const imgSrc = `${thumbBase}${d.original_id}.jpg`;
175
+ const userText = truncateText(d.user);
176
+ const assistantText = truncateText(d.assistant);
177
+ const layoutAdjust = isMobileLayout() ? 'flex-direction:column; align-items:center;' : '';
178
+ tipInner.innerHTML =
179
+ `<div style="display:flex; gap:10px; align-items:flex-start; ${layoutAdjust}">` +
180
+ `<div style="width:120px;height:120px;flex:0 0 auto;border-radius:6px;border:1px solid var(--border-color);display:flex;align-items:center;justify-content:center;overflow:hidden;background:var(--surface-bg);">` +
181
+ `<img src="${imgSrc}" alt="thumb ${d.original_id}" style="max-width:100%;max-height:100%;object-fit:contain;display:block;" />` +
182
+ `</div>` +
183
+ `<div style="min-width:140px; max-width:200px;">` +
184
+ `<div><strong>${d.category || 'Unknown'}</strong></div>` +
185
+ `<div style="word-wrap:break-word; line-height:1.3; margin:4px 0;"><strong>Q:</strong> ${userText}</div>` +
186
+ `<div style="word-wrap:break-word; line-height:1.3; margin:4px 0;"><strong>A:</strong> ${assistantText}</div>` +
187
+ `<div><strong>Subset</strong> ${d.subset ?? '—'}</div>` +
188
+ `</div>` +
189
+ `</div>`;
190
+ };
191
 
192
  // Centroides par catégorie (moyenne x/y)
193
  const centroids = Array.from(
 
339
  .attr('stroke', (d) => pointStroke(d))
340
  .attr('stroke-width', 0.4)
341
  .on('mouseenter', function(ev, d) {
342
+ if (isMobileLayout()) { updateTooltipContent(d); return; }
343
  d3.select(this).raise()
344
  .attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
345
  .attr('stroke-width', 1.2);
346
+ updateTooltipContent(d);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  tip.style.opacity = '1';
348
+ const [mx, my] = d3.pointer(ev, container);
349
+ positionTooltipWithinBounds(mx, my);
350
  })
351
  .on('mousemove', (ev) => {
352
+ if (isMobileLayout()) return;
353
  const [mx, my] = d3.pointer(ev, container);
354
+ positionTooltipWithinBounds(mx, my);
 
355
  })
356
  .on('mouseleave', function() {
357
+ if (isMobileLayout()) return;
358
  tip.style.opacity = '0';
359
  tip.style.transform = 'translate(-9999px, -9999px)';
360
  d3.select(this)
361
  .attr('stroke', (d) => pointStroke(d))
362
  .attr('stroke-width', 0.4);
363
+ })
364
+ .on('click', function(ev, d) {
365
+ updateTooltipContent(d);
366
+ if (isMobileLayout()) {
367
+ tipVisible = true;
368
+ applyTooltipLayout();
369
+ }
370
  }),
371
  (update) => update
372
  .attr('cx', (d) => xScale(d.x_position))
 
377
  .attr('stroke', (d) => pointStroke(d))
378
  .attr('stroke-width', 0.4)
379
  );
380
+ // Ensure tooltip layout reflects current viewport
381
+ applyTooltipLayout();
382
+ applyContainerLayout();
383
  };
384
 
385
  if (window.ResizeObserver) {
386
+ const ro = new ResizeObserver(() => {
387
+ render();
388
+ applyTooltipLayout();
389
+ applyContainerLayout();
390
+ });
391
  ro.observe(container);
392
  } else {
393
+ window.addEventListener('resize', () => { render(); applyTooltipLayout(); applyContainerLayout(); });
394
  }
395
  // Re-render on theme changes (data-theme attribute updates live)
396
  const themeObserver = new MutationObserver((mutations) => {
 
403
  });
404
  themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
405
  render();
406
+ applyTooltipLayout();
407
+ applyContainerLayout();
408
  });
409
  };
410