tfrere HF Staff commited on
Commit
7631c4e
·
1 Parent(s): 52d0821

fix file size in line charts, update wrong reasons, add train model decision flowchart

Browse files
app/src/components/Image.astro CHANGED
@@ -42,8 +42,40 @@ const {
42
  linkTarget,
43
  linkRel,
44
  fullWidth,
 
45
  ...imgProps
46
  } = Astro.props as Props;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  const hasCaptionSlot = Astro.slots.has("caption");
48
  const hasCaption =
49
  hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
@@ -56,6 +88,9 @@ const dataDownloadable =
56
  const hasLink = typeof linkHref === "string" && linkHref.length > 0;
57
  const resolvedTarget = hasLink ? linkTarget || "_blank" : undefined;
58
  const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
 
 
 
59
  ---
60
 
61
  <div
@@ -79,20 +114,22 @@ const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
79
  rel={resolvedRel}
80
  >
81
  <AstroImage
 
82
  {...imgProps}
83
  data-zoomable={dataZoomable}
84
  data-downloadable={dataDownloadable}
85
  data-download-name={downloadName}
86
- data-download-src={downloadSrc}
87
  />
88
  </a>
89
  ) : (
90
  <AstroImage
 
91
  {...imgProps}
92
  data-zoomable={dataZoomable}
93
  data-downloadable={dataDownloadable}
94
  data-download-name={downloadName}
95
- data-download-src={downloadSrc}
96
  />
97
  )}
98
  <button
@@ -115,10 +152,10 @@ const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
115
  target={resolvedTarget}
116
  rel={resolvedRel}
117
  >
118
- <AstroImage {...imgProps} data-zoomable={dataZoomable} />
119
  </a>
120
  ) : (
121
- <AstroImage {...imgProps} data-zoomable={dataZoomable} />
122
  )}
123
  <figcaption>
124
  {hasCaptionSlot ? (
@@ -138,20 +175,22 @@ const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
138
  rel={resolvedRel}
139
  >
140
  <AstroImage
 
141
  {...imgProps}
142
  data-zoomable={dataZoomable}
143
  data-downloadable={dataDownloadable}
144
  data-download-name={downloadName}
145
- data-download-src={downloadSrc}
146
  />
147
  </a>
148
  ) : (
149
  <AstroImage
 
150
  {...imgProps}
151
  data-zoomable={dataZoomable}
152
  data-downloadable={dataDownloadable}
153
  data-download-name={downloadName}
154
- data-download-src={downloadSrc}
155
  />
156
  )}
157
  <button
@@ -173,6 +212,7 @@ const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
173
  rel={resolvedRel}
174
  >
175
  <AstroImage
 
176
  {...imgProps}
177
  data-zoomable={dataZoomable}
178
  class={fullWidth ? "full" : ""}
@@ -180,6 +220,7 @@ const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
180
  </a>
181
  ) : (
182
  <AstroImage
 
183
  {...imgProps}
184
  data-zoomable={dataZoomable}
185
  class={fullWidth ? "full" : ""}
 
42
  linkTarget,
43
  linkRel,
44
  fullWidth,
45
+ src,
46
  ...imgProps
47
  } = Astro.props as Props;
48
+
49
+ // Try to get original image src from ImageMetadata object
50
+ const getOriginalSrc = (imageSrc: any): string => {
51
+ if (typeof imageSrc === "string") {
52
+ return imageSrc;
53
+ }
54
+ // ImageMetadata from astro:assets has a 'src' property with the URL
55
+ if (imageSrc && typeof imageSrc === "object") {
56
+ // Check if this is an import with ?url (returns the original path directly)
57
+ if ("default" in imageSrc && typeof imageSrc.default === "string") {
58
+ return imageSrc.default;
59
+ }
60
+ // Try src property (optimized version)
61
+ if ("src" in imageSrc && typeof imageSrc.src === "string") {
62
+ return imageSrc.src;
63
+ }
64
+ // Try pathname property
65
+ if ("pathname" in imageSrc && typeof imageSrc.pathname === "string") {
66
+ return imageSrc.pathname;
67
+ }
68
+ // Fallback: try all string properties
69
+ for (const key of Object.keys(imageSrc)) {
70
+ if (typeof imageSrc[key] === "string" && imageSrc[key].length > 0) {
71
+ return imageSrc[key];
72
+ }
73
+ }
74
+ }
75
+ return "";
76
+ };
77
+
78
+ const originalSrc = getOriginalSrc(src);
79
  const hasCaptionSlot = Astro.slots.has("caption");
80
  const hasCaption =
81
  hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
 
88
  const hasLink = typeof linkHref === "string" && linkHref.length > 0;
89
  const resolvedTarget = hasLink ? linkTarget || "_blank" : undefined;
90
  const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
91
+
92
+ // Use downloadSrc if provided, otherwise try to use the original src
93
+ const resolvedDownloadSrc = downloadSrc || originalSrc;
94
  ---
95
 
96
  <div
 
114
  rel={resolvedRel}
115
  >
116
  <AstroImage
117
+ src={src}
118
  {...imgProps}
119
  data-zoomable={dataZoomable}
120
  data-downloadable={dataDownloadable}
121
  data-download-name={downloadName}
122
+ data-download-src={resolvedDownloadSrc}
123
  />
124
  </a>
125
  ) : (
126
  <AstroImage
127
+ src={src}
128
  {...imgProps}
129
  data-zoomable={dataZoomable}
130
  data-downloadable={dataDownloadable}
131
  data-download-name={downloadName}
132
+ data-download-src={resolvedDownloadSrc}
133
  />
134
  )}
135
  <button
 
152
  target={resolvedTarget}
153
  rel={resolvedRel}
154
  >
155
+ <AstroImage src={src} {...imgProps} data-zoomable={dataZoomable} />
156
  </a>
157
  ) : (
158
+ <AstroImage src={src} {...imgProps} data-zoomable={dataZoomable} />
159
  )}
160
  <figcaption>
161
  {hasCaptionSlot ? (
 
175
  rel={resolvedRel}
176
  >
177
  <AstroImage
178
+ src={src}
179
  {...imgProps}
180
  data-zoomable={dataZoomable}
181
  data-downloadable={dataDownloadable}
182
  data-download-name={downloadName}
183
+ data-download-src={resolvedDownloadSrc}
184
  />
185
  </a>
186
  ) : (
187
  <AstroImage
188
+ src={src}
189
  {...imgProps}
190
  data-zoomable={dataZoomable}
191
  data-downloadable={dataDownloadable}
192
  data-download-name={downloadName}
193
+ data-download-src={resolvedDownloadSrc}
194
  />
195
  )}
196
  <button
 
212
  rel={resolvedRel}
213
  >
214
  <AstroImage
215
+ src={src}
216
  {...imgProps}
217
  data-zoomable={dataZoomable}
218
  class={fullWidth ? "full" : ""}
 
220
  </a>
221
  ) : (
222
  <AstroImage
223
+ src={src}
224
  {...imgProps}
225
  data-zoomable={dataZoomable}
226
  class={fullWidth ? "full" : ""}
app/src/components/Note.astro CHANGED
@@ -54,7 +54,8 @@ const hasHeader =
54
  .note {
55
  background: var(--surface-bg);
56
  border-left: 2px solid var(--border-color);
57
- border-radius: 4px;
 
58
  padding: 10px 14px;
59
  margin: var(--block-spacing-y) 0;
60
  }
 
54
  .note {
55
  background: var(--surface-bg);
56
  border-left: 2px solid var(--border-color);
57
+ border-radius-top-right: 4px;
58
+ border-radius-bottom-right: 4px;
59
  padding: 10px 14px;
60
  margin: var(--block-spacing-y) 0;
61
  }
app/src/content/embeds/d3-line-chart.html CHANGED
@@ -204,34 +204,7 @@
204
  flex-wrap: wrap;
205
  }
206
 
207
- /* Trackio footer */
208
- .d3-line-chart .trackio-footer {
209
- text-align: left;
210
- margin-top: 12px;
211
- margin-bottom: -2px;
212
- padding: 0;
213
- opacity: 0.35;
214
- }
215
-
216
- @media (prefers-color-scheme: light) {
217
- .d3-line-chart .trackio-footer {
218
- opacity: 0.7;
219
- }
220
- }
221
-
222
- .d3-line-chart .trackio-footer a {
223
- font-size: 9px;
224
- color: var(--muted-color);
225
- opacity: 1;
226
- text-decoration: none;
227
- font-weight: 400;
228
- letter-spacing: 0.3px;
229
- transition: opacity 0.2s ease;
230
- }
231
-
232
- .d3-line-chart .trackio-footer a:hover {
233
- opacity: 0.6;
234
- }
235
  </style>
236
  <script>
237
  (() => {
@@ -320,11 +293,7 @@
320
  container.appendChild(chartCard);
321
  container.appendChild(header);
322
 
323
- // Add Trackio footer
324
- const footer = document.createElement('div');
325
- footer.className = 'trackio-footer';
326
- footer.innerHTML = '<a href="https://github.com/gradio-app/trackio" target="_blank" rel="noopener noreferrer">Made with Trackio</a>';
327
- container.appendChild(footer);
328
 
329
  const d3 = window.d3;
330
 
@@ -471,6 +440,19 @@
471
  // Smart formatters (will be set after data loading)
472
  let formatX = (v) => v;
473
  let formatY = (v) => v;
 
 
 
 
 
 
 
 
 
 
 
 
 
474
 
475
  // Function to determine smart format based on data values
476
  function createSmartFormatter(values, isFileSize = false) {
@@ -483,18 +465,21 @@
483
  // Check if all values are effectively integers (within 0.001 tolerance)
484
  const allIntegers = values.every(v => Math.abs(v - Math.round(v)) < 0.001);
485
 
486
- // File size formatting (B, kB, MB, GB)
487
  if (isFileSize) {
 
 
 
488
  return (v) => {
489
- if (v >= 1e9) {
490
- const gb = v / 1e9;
491
- return d3.format('.2f')(gb) + ' GB';
492
- } else if (v >= 1e6) {
493
- const mb = v / 1e6;
494
- return d3.format('.2f')(mb) + ' MB';
495
- } else if (v >= 1000) {
496
- const kb = v / 1000;
497
- return d3.format('.1f')(kb) + ' kB';
498
  } else {
499
  return d3.format('d')(Math.round(v)) + ' B';
500
  }
@@ -574,9 +559,10 @@
574
  const innerWidth = xScale.range()[1];
575
 
576
  // Smart grid ticks (same as axis ticks)
 
577
  const getGridTicks = (scale, scaleType) => {
578
  if (scaleType !== 'log') {
579
- return scale.ticks(6);
580
  }
581
 
582
  const domain = scale.domain();
@@ -605,7 +591,8 @@
605
  ticks.sort((a, b) => a - b);
606
  }
607
 
608
- return ticks.filter(t => t >= domain[0] && t <= domain[1]);
 
609
  };
610
 
611
  // Update grid lines
@@ -640,18 +627,27 @@
640
  .attr('d', d => line(applySmoothing(d.values, smoothEnabled)));
641
 
642
  // Smart tick generation for zoomed view
643
- const getSmartTicksZoom = (scale, scaleType) => {
644
  if (scaleType !== 'log') {
645
- return scale.ticks(6);
646
  }
647
 
648
  const domain = scale.domain();
 
 
 
 
 
 
 
 
 
649
  const minLog = Math.log10(domain[0]);
650
  const maxLog = Math.log10(domain[1]);
651
  const logRange = maxLog - minLog;
652
 
653
  if (logRange < 2) {
654
- return scale.ticks(Math.min(4, Math.ceil(logRange * 2)));
655
  }
656
 
657
  const ticks = [];
@@ -671,10 +667,11 @@
671
  ticks.sort((a, b) => a - b);
672
  }
673
 
674
- return ticks.filter(t => t >= domain[0] && t <= domain[1]);
 
675
  };
676
 
677
- const newXTicks = getSmartTicksZoom(newXScale, CONFIG.xScaleType);
678
  const newYTicks = getSmartTicksZoom(newYScale, CONFIG.yScaleType);
679
 
680
  // Update axes with rescaled domains
@@ -742,33 +739,38 @@
742
  .translateExtent([[0, 0], [innerWidth, innerHeight]]);
743
 
744
  // Smart tick generation for log scales
745
- const getSmartTicks = (scale, scaleType, targetCount = 6) => {
746
  if (scaleType !== 'log') {
747
- return scale.ticks(targetCount);
748
  }
749
 
750
- // For log scale, use custom tick logic to avoid crowding
751
  const domain = scale.domain();
 
 
 
 
 
 
 
 
 
 
752
  const minLog = Math.log10(domain[0]);
753
  const maxLog = Math.log10(domain[1]);
754
  const logRange = maxLog - minLog;
755
 
756
- // If range is small (< 2 orders of magnitude), use fewer ticks
757
  if (logRange < 2) {
758
- return scale.ticks(Math.min(4, Math.ceil(logRange * 2)));
759
  }
760
 
761
- // For larger ranges, show powers of 10 and maybe intermediate values
762
  const ticks = [];
763
  const minPower = Math.ceil(minLog);
764
  const maxPower = Math.floor(maxLog);
765
 
766
- // Always include powers of 10
767
  for (let power = minPower; power <= maxPower; power++) {
768
  ticks.push(Math.pow(10, power));
769
  }
770
 
771
- // If we have too few ticks and the range is not too large, add intermediate values
772
  if (ticks.length < 4 && logRange < 3) {
773
  const intermediateTicks = [];
774
  for (let power = minPower; power < maxPower; power++) {
@@ -778,11 +780,11 @@
778
  ticks.sort((a, b) => a - b);
779
  }
780
 
781
- // Filter ticks to be within domain
782
- return ticks.filter(t => t >= domain[0] && t <= domain[1]);
783
  };
784
 
785
- const xTicks = getSmartTicks(xScale, CONFIG.xScaleType, 6);
786
  const yTicks = getSmartTicks(yScale, CONFIG.yScaleType, 6);
787
 
788
  // Grid (use smart ticks)
@@ -1116,6 +1118,9 @@
1116
  CONFIG.xColumn.toLowerCase().includes('size') ||
1117
  CONFIG.xColumn.toLowerCase().includes('message');
1118
 
 
 
 
1119
  formatX = createSmartFormatter(xValues, shouldFormatAsFileSize);
1120
  formatY = createSmartFormatter(yValues);
1121
 
 
204
  flex-wrap: wrap;
205
  }
206
 
207
+ /* Trackio footer removed */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  </style>
209
  <script>
210
  (() => {
 
293
  container.appendChild(chartCard);
294
  container.appendChild(header);
295
 
296
+ // Footer removed
 
 
 
 
297
 
298
  const d3 = window.d3;
299
 
 
440
  // Smart formatters (will be set after data loading)
441
  let formatX = (v) => v;
442
  let formatY = (v) => v;
443
+ const MAX_TICKS = 10;
444
+
445
+ function limitTicks(ticks, maxCount, domain) {
446
+ if (!Array.isArray(ticks)) return ticks;
447
+ if (ticks.length <= maxCount) return ticks;
448
+ const first = ticks[0];
449
+ const last = ticks[ticks.length - 1];
450
+ const step = Math.ceil(ticks.length / maxCount);
451
+ const sampled = ticks.filter((_, i) => i % step === 0);
452
+ if (sampled[0] !== first && first >= domain[0]) sampled.unshift(first);
453
+ if (sampled[sampled.length - 1] !== last && last <= domain[1]) sampled.push(last);
454
+ return sampled.slice(0, maxCount);
455
+ }
456
 
457
  // Function to determine smart format based on data values
458
  function createSmartFormatter(values, isFileSize = false) {
 
465
  // Check if all values are effectively integers (within 0.001 tolerance)
466
  const allIntegers = values.every(v => Math.abs(v - Math.round(v)) < 0.001);
467
 
468
+ // File size formatting (binary units: B, KiB, MiB, GiB)
469
  if (isFileSize) {
470
+ const KiB = 1024;
471
+ const MiB = KiB * 1024;
472
+ const GiB = MiB * 1024;
473
  return (v) => {
474
+ if (v >= GiB) {
475
+ const gib = v / GiB;
476
+ return (gib % 1 === 0 ? d3.format('d')(gib) : d3.format('.2f')(gib)) + ' GiB';
477
+ } else if (v >= MiB) {
478
+ const mib = v / MiB;
479
+ return (mib % 1 === 0 ? d3.format('d')(mib) : d3.format('.2f')(mib)) + ' MiB';
480
+ } else if (v >= KiB) {
481
+ const kib = v / KiB;
482
+ return (kib % 1 === 0 ? d3.format('d')(kib) : d3.format('.1f')(kib)) + ' KiB';
483
  } else {
484
  return d3.format('d')(Math.round(v)) + ' B';
485
  }
 
559
  const innerWidth = xScale.range()[1];
560
 
561
  // Smart grid ticks (same as axis ticks)
562
+
563
  const getGridTicks = (scale, scaleType) => {
564
  if (scaleType !== 'log') {
565
+ return scale.ticks(Math.min(6, MAX_TICKS));
566
  }
567
 
568
  const domain = scale.domain();
 
591
  ticks.sort((a, b) => a - b);
592
  }
593
 
594
+ const within = ticks.filter(t => t >= domain[0] && t <= domain[1]);
595
+ return limitTicks(within, MAX_TICKS, domain);
596
  };
597
 
598
  // Update grid lines
 
627
  .attr('d', d => line(applySmoothing(d.values, smoothEnabled)));
628
 
629
  // Smart tick generation for zoomed view
630
+ const getSmartTicksZoom = (scale, scaleType, useBinary = false) => {
631
  if (scaleType !== 'log') {
632
+ return scale.ticks(Math.min(6, MAX_TICKS));
633
  }
634
 
635
  const domain = scale.domain();
636
+ if (useBinary) {
637
+ const minPow2 = Math.ceil(Math.log2(domain[0]));
638
+ const maxPow2 = Math.floor(Math.log2(domain[1]));
639
+ const ticks = [];
640
+ for (let p = minPow2; p <= maxPow2; p++) ticks.push(Math.pow(2, p));
641
+ const within = ticks.filter(t => t >= domain[0] && t <= domain[1]);
642
+ return limitTicks(within, MAX_TICKS, domain);
643
+ }
644
+
645
  const minLog = Math.log10(domain[0]);
646
  const maxLog = Math.log10(domain[1]);
647
  const logRange = maxLog - minLog;
648
 
649
  if (logRange < 2) {
650
+ return scale.ticks(Math.min(MAX_TICKS, Math.min(4, Math.ceil(logRange * 2))));
651
  }
652
 
653
  const ticks = [];
 
667
  ticks.sort((a, b) => a - b);
668
  }
669
 
670
+ const within = ticks.filter(t => t >= domain[0] && t <= domain[1]);
671
+ return limitTicks(within, MAX_TICKS, domain);
672
  };
673
 
674
+ const newXTicks = getSmartTicksZoom(newXScale, CONFIG.xScaleType, !!CONFIG.xFormatAsFileSize);
675
  const newYTicks = getSmartTicksZoom(newYScale, CONFIG.yScaleType);
676
 
677
  // Update axes with rescaled domains
 
739
  .translateExtent([[0, 0], [innerWidth, innerHeight]]);
740
 
741
  // Smart tick generation for log scales
742
+ const getSmartTicks = (scale, scaleType, targetCount = 6, useBinary = false) => {
743
  if (scaleType !== 'log') {
744
+ return scale.ticks(Math.min(targetCount, MAX_TICKS));
745
  }
746
 
 
747
  const domain = scale.domain();
748
+ if (useBinary) {
749
+ const minPow2 = Math.ceil(Math.log2(domain[0]));
750
+ const maxPow2 = Math.floor(Math.log2(domain[1]));
751
+ const ticks = [];
752
+ for (let p = minPow2; p <= maxPow2; p++) ticks.push(Math.pow(2, p));
753
+ const within = ticks.filter(t => t >= domain[0] && t <= domain[1]);
754
+ return limitTicks(within, MAX_TICKS, domain);
755
+ }
756
+
757
+ // For log scale, use custom tick logic to avoid crowding (powers of 10)
758
  const minLog = Math.log10(domain[0]);
759
  const maxLog = Math.log10(domain[1]);
760
  const logRange = maxLog - minLog;
761
 
 
762
  if (logRange < 2) {
763
+ return scale.ticks(Math.min(MAX_TICKS, Math.min(4, Math.ceil(logRange * 2))));
764
  }
765
 
 
766
  const ticks = [];
767
  const minPower = Math.ceil(minLog);
768
  const maxPower = Math.floor(maxLog);
769
 
 
770
  for (let power = minPower; power <= maxPower; power++) {
771
  ticks.push(Math.pow(10, power));
772
  }
773
 
 
774
  if (ticks.length < 4 && logRange < 3) {
775
  const intermediateTicks = [];
776
  for (let power = minPower; power < maxPower; power++) {
 
780
  ticks.sort((a, b) => a - b);
781
  }
782
 
783
+ const within = ticks.filter(t => t >= domain[0] && t <= domain[1]);
784
+ return limitTicks(within, MAX_TICKS, domain);
785
  };
786
 
787
+ const xTicks = getSmartTicks(xScale, CONFIG.xScaleType, 6, !!CONFIG.xFormatAsFileSize);
788
  const yTicks = getSmartTicks(yScale, CONFIG.yScaleType, 6);
789
 
790
  // Grid (use smart ticks)
 
1118
  CONFIG.xColumn.toLowerCase().includes('size') ||
1119
  CONFIG.xColumn.toLowerCase().includes('message');
1120
 
1121
+ // Ensure CONFIG flag reflects detection for consistent tick logic
1122
+ CONFIG.xFormatAsFileSize = shouldFormatAsFileSize;
1123
+
1124
  formatX = createSmartFormatter(xValues, shouldFormatAsFileSize);
1125
  formatY = createSmartFormatter(yValues);
1126
 
app/src/content/embeds/train-model-decision-flowchart.html ADDED
@@ -0,0 +1,504 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!--
2
+ Train Model Decision Flowchart
3
+
4
+ Usage:
5
+ <HtmlEmbed src="/embeds/train-model-decision-flowchart.html" />
6
+ -->
7
+ <div class="train-model-decision-flowchart"></div>
8
+ <style>
9
+ .train-model-decision-flowchart {
10
+ width: 100%;
11
+ min-height: 300px;
12
+ position: relative;
13
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
14
+ }
15
+
16
+ .train-model-decision-flowchart svg {
17
+ display: block;
18
+ width: 100%;
19
+ }
20
+
21
+ .train-model-decision-flowchart .node-rect {
22
+ stroke-width: 2px;
23
+ rx: 12px;
24
+ ry: 12px;
25
+ }
26
+
27
+ .train-model-decision-flowchart .node-text {
28
+ font-size: 18px;
29
+ font-weight: 500;
30
+ text-anchor: middle;
31
+ pointer-events: none;
32
+ fill: var(--text-color, #333);
33
+ }
34
+
35
+ .train-model-decision-flowchart .node-question {
36
+ fill: color-mix(in srgb, var(--primary-color) 30%, var(--page-bg));
37
+ }
38
+
39
+ .train-model-decision-flowchart .node-success {
40
+ fill: color-mix(in srgb, var(--success-color) 30%, var(--page-bg));
41
+ }
42
+
43
+ .train-model-decision-flowchart .node-category {
44
+ fill: color-mix(in srgb, var(--danger-color) 25%, var(--page-bg));
45
+ }
46
+
47
+ .train-model-decision-flowchart .link-path {
48
+ fill: none;
49
+ stroke: var(--muted-color, #666);
50
+ stroke-width: 2.5px;
51
+ marker-end: url(#arrowhead);
52
+ }
53
+
54
+ .train-model-decision-flowchart .link-label {
55
+ font-size: 14px;
56
+ font-weight: 700;
57
+ fill: var(--text-color, #333);
58
+ text-anchor: middle;
59
+ pointer-events: none;
60
+ }
61
+
62
+ .train-model-decision-flowchart .link-label-bg {
63
+ fill: var(--page-bg, #ffffff);
64
+ stroke: none;
65
+ }
66
+ </style>
67
+ <script>
68
+ (() => {
69
+ const ensureD3 = (cb) => {
70
+ if (window.d3 && typeof window.d3.select === 'function') return cb();
71
+ let s = document.getElementById('d3-cdn-script');
72
+ if (!s) {
73
+ s = document.createElement('script');
74
+ s.id = 'd3-cdn-script';
75
+ s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
76
+ document.head.appendChild(s);
77
+ }
78
+ const onReady = () => {
79
+ if (window.d3 && typeof window.d3.select === 'function') cb();
80
+ };
81
+ s.addEventListener('load', onReady, { once: true });
82
+ if (window.d3) onReady();
83
+ };
84
+
85
+ const bootstrap = () => {
86
+ const scriptEl = document.currentScript;
87
+ let container = scriptEl ? scriptEl.previousElementSibling : null;
88
+ if (!(container && container.classList && container.classList.contains('train-model-decision-flowchart'))) {
89
+ const candidates = Array.from(document.querySelectorAll('.train-model-decision-flowchart'))
90
+ .filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
91
+ container = candidates[candidates.length - 1] || null;
92
+ }
93
+ if (!container) return;
94
+ if (container.dataset) {
95
+ if (container.dataset.mounted === 'true') return;
96
+ container.dataset.mounted = 'true';
97
+ }
98
+
99
+ // Define color scheme matching the Mermaid chart
100
+ const getColors = () => {
101
+ // Try to use CSS variables first, fallback to Mermaid colors
102
+ const getCSSVar = (varName, fallback) => {
103
+ if (typeof getComputedStyle !== 'undefined') {
104
+ const value = getComputedStyle(document.documentElement)
105
+ .getPropertyValue(varName);
106
+ if (value && value.trim()) {
107
+ return value.trim();
108
+ }
109
+ }
110
+ return fallback;
111
+ };
112
+
113
+ return {
114
+ question: getCSSVar('--danger-color', '#e85c42'),
115
+ decision: getCSSVar('--surface-bg', '#f9f9f9'),
116
+ success: getCSSVar('--success-color', '#42d9b3'),
117
+ category: getCSSVar('--primary-color', '#0084ff'),
118
+ link: getCSSVar('--muted-color', '#666')
119
+ };
120
+ };
121
+
122
+ // Define the flowchart structure
123
+ // Layout: top-down with very generous vertical spacing between layers
124
+ const nodes = [
125
+ { id: 'A', label: 'Should you train your own model?', type: 'question', x: 400, y: 100 },
126
+ { id: 'B', label: 'Can existing models handle your use case?', type: 'question', x: 400, y: 240 },
127
+ { id: 'C', label: 'Existing models work well just with prompting', type: 'decision', x: 200, y: 400 },
128
+ { id: 'D', label: 'Prompting isn\'t enough', type: 'decision', x: 600, y: 400 },
129
+ { id: 'E', label: '❌\nDon\'t train. Use existing models', type: 'success', x: 200, y: 560 },
130
+ { id: 'F', label: 'Can finetuning solve your problem?', type: 'question', x: 600, y: 600 },
131
+ { id: 'G', label: 'Finetuning works (post-training/continual pretraining)', type: 'decision', x: 450, y: 760 },
132
+ { id: 'H', label: 'Finetuning cannot solve your problem', type: 'decision', x: 750, y: 760 },
133
+ { id: 'I', label: '❌\nDon\'t train from scratch', type: 'success', x: 450, y: 920 },
134
+ { id: 'J', label: 'Train a model under one of these categories', type: 'category', x: 750, y: 960 },
135
+ { id: 'K', label: '🔬\nResearch', type: 'category', x: 600, y: 1120 },
136
+ { id: 'L', label: '🏭\nProduction', type: 'category', x: 750, y: 1120 },
137
+ { id: 'N', label: '🌐\nStrategic Open-Source', type: 'category', x: 970, y: 1120 }
138
+ ];
139
+
140
+ const links = [
141
+ { source: 'A', target: 'B', label: '' },
142
+ { source: 'B', target: 'C', label: 'YES' },
143
+ { source: 'B', target: 'D', label: 'NO' },
144
+ { source: 'C', target: 'E', label: '' },
145
+ { source: 'D', target: 'F', label: '' },
146
+ { source: 'F', target: 'G', label: 'YES' },
147
+ { source: 'F', target: 'H', label: 'NO' },
148
+ { source: 'G', target: 'I', label: '' },
149
+ { source: 'H', target: 'J', label: '' },
150
+ { source: 'J', target: 'K', label: '' },
151
+ { source: 'J', target: 'L', label: '' },
152
+ { source: 'J', target: 'N', label: '' }
153
+ ];
154
+
155
+ // Create SVG
156
+ const svg = d3.select(container).append('svg').attr('width', '100%').style('display', 'block');
157
+ const gRoot = svg.append('g');
158
+
159
+ // Define arrowhead marker (solid triangle arrowhead)
160
+ const defs = svg.append('defs');
161
+ const marker = defs.append('marker')
162
+ .attr('id', 'arrowhead')
163
+ .attr('viewBox', '0 0 10 10')
164
+ .attr('refX', 2.5)
165
+ .attr('refY', 5)
166
+ .attr('markerWidth', 4)
167
+ .attr('markerHeight', 4)
168
+ .attr('orient', 'auto');
169
+
170
+ // Create solid arrowhead pointing right (smaller)
171
+ marker.append('path')
172
+ .attr('d', 'M 0 0 L 8 5 L 0 10 Z')
173
+ .attr('fill', () => getColors().link);
174
+
175
+ let width = 1000, height = 800;
176
+
177
+ function render() {
178
+ width = container.clientWidth || 1000;
179
+ height = Math.max(800, Math.round(width * 1.3));
180
+ svg.attr('width', width).attr('height', height);
181
+
182
+ const colors = getColors();
183
+
184
+ // Calculate scale to fit content (no padding, allow to touch edges)
185
+ const nodeExtent = {
186
+ minX: d3.min(nodes, d => d.x) - 140,
187
+ maxX: d3.max(nodes, d => d.x) + 140,
188
+ minY: d3.min(nodes, d => d.y) - 20,
189
+ maxY: d3.max(nodes, d => d.y) + 20
190
+ };
191
+
192
+ const contentWidth = nodeExtent.maxX - nodeExtent.minX;
193
+ const contentHeight = nodeExtent.maxY - nodeExtent.minY;
194
+
195
+ const scale = Math.min(width / contentWidth, height / contentHeight);
196
+ const offsetX = (width - contentWidth * scale) / 2 - nodeExtent.minX * scale;
197
+ const offsetY = (height - contentHeight * scale) / 2 - nodeExtent.minY * scale;
198
+
199
+ gRoot.attr('transform', `translate(${offsetX}, ${offsetY}) scale(${scale})`);
200
+
201
+ // Create a temporary text element for measuring text width
202
+ const tempText = gRoot.append('text')
203
+ .style('visibility', 'hidden')
204
+ .style('font-size', '18px')
205
+ .style('font-weight', '500');
206
+
207
+ // Word wrap function - intelligently breaks text into lines
208
+ const wordWrap = (text, maxWidth, fontSize = '18px') => {
209
+ const explicitLines = text.split('\n');
210
+ const wrappedLines = [];
211
+
212
+ explicitLines.forEach(line => {
213
+ if (!line.trim()) {
214
+ wrappedLines.push(line);
215
+ return;
216
+ }
217
+
218
+ tempText.attr('font-size', fontSize).text(line);
219
+ const textWidth = tempText.node().getComputedTextLength();
220
+
221
+ // If line fits, keep it as is
222
+ if (textWidth <= maxWidth) {
223
+ wrappedLines.push(line);
224
+ return;
225
+ }
226
+
227
+ // Otherwise, break into words and wrap
228
+ const words = line.split(/\s+/);
229
+ let currentLine = '';
230
+
231
+ words.forEach(word => {
232
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
233
+ tempText.text(testLine);
234
+ const testWidth = tempText.node().getComputedTextLength();
235
+
236
+ if (testWidth <= maxWidth && currentLine) {
237
+ currentLine = testLine;
238
+ } else {
239
+ if (currentLine) {
240
+ wrappedLines.push(currentLine);
241
+ }
242
+ currentLine = word;
243
+ }
244
+ });
245
+
246
+ if (currentLine) {
247
+ wrappedLines.push(currentLine);
248
+ }
249
+ });
250
+
251
+ return wrappedLines.filter(line => line.trim().length > 0);
252
+ };
253
+
254
+ // Calculate node dimensions with word wrapping
255
+ const getNodeDimensions = (node) => {
256
+ // Determine max width based on node type (increased for better readability)
257
+ const maxWidths = {
258
+ question: 220,
259
+ decision: 240,
260
+ success: 220,
261
+ category: 200
262
+ };
263
+
264
+ const maxWidth = maxWidths[node.type] || 180;
265
+ const fontSize = node.type === 'category' ? '19px' : '18px';
266
+
267
+ // Get wrapped lines
268
+ const wrappedLines = wordWrap(node.label, maxWidth, fontSize);
269
+ node.wrappedLines = wrappedLines;
270
+
271
+ // Calculate actual width from wrapped lines
272
+ tempText.attr('font-size', fontSize);
273
+ const lineWidths = wrappedLines.map(line => {
274
+ tempText.text(line);
275
+ return tempText.node().getComputedTextLength();
276
+ });
277
+ const maxLineWidth = Math.max(...lineWidths, 0);
278
+
279
+ // Calculate dimensions with padding
280
+ const padding = 36;
281
+ const lineHeight = node.type === 'category' ? 28 : 26;
282
+
283
+ const width = Math.max(120, maxLineWidth + padding);
284
+ const height = Math.max(30, wrappedLines.length * lineHeight + padding);
285
+
286
+ return { width, height, wrappedLines };
287
+ };
288
+
289
+ // Pre-calculate all node dimensions with wrapping
290
+ nodes.forEach(node => {
291
+ const dims = getNodeDimensions(node);
292
+ node.width = dims.width;
293
+ node.height = dims.height;
294
+ });
295
+
296
+ // Keep tempText for measuring link labels (don't remove it yet)
297
+
298
+ // Draw links first (so labels can be on top)
299
+ const linkGroup = gRoot.selectAll('.link-group').data(links);
300
+ const linkEnter = linkGroup.enter().append('g').attr('class', 'link-group');
301
+
302
+ linkEnter.append('path').attr('class', 'link-path');
303
+ // Add background rect and text for labels (will be positioned later, after path)
304
+ linkEnter.append('rect').attr('class', 'link-label-bg').style('opacity', 0);
305
+ linkEnter.append('text').attr('class', 'link-label').attr('dy', -5);
306
+
307
+ const linkMerge = linkEnter.merge(linkGroup);
308
+
309
+ linkMerge.select('.link-path')
310
+ .attr('d', d => {
311
+ const sourceNode = nodes.find(n => n.id === d.source);
312
+ const targetNode = nodes.find(n => n.id === d.target);
313
+
314
+ // Add gap between arrows and nodes, and ensure arrows point to node edges
315
+ const gap = 12;
316
+
317
+ // Calculate arrow start/end points at node edges
318
+ // For vertical connections
319
+ if (Math.abs(sourceNode.x - targetNode.x) < 50) {
320
+ const sx = sourceNode.x;
321
+ const sy = sourceNode.y + sourceNode.height / 2 + gap;
322
+ const tx = targetNode.x;
323
+ const ty = targetNode.y - targetNode.height / 2 - gap;
324
+ return `M ${sx} ${sy} L ${tx} ${ty}`;
325
+ }
326
+
327
+ // For horizontal/curved connections
328
+ // Determine connection points based on relative positions
329
+ let sx, sy, tx, ty;
330
+
331
+ if (Math.abs(sourceNode.y - targetNode.y) < 50) {
332
+ // Horizontal connection
333
+ const sourceIsLeft = sourceNode.x < targetNode.x;
334
+ sx = sourceNode.x + (sourceIsLeft ? sourceNode.width / 2 + gap : -(sourceNode.width / 2 + gap));
335
+ sy = sourceNode.y;
336
+ tx = targetNode.x + (sourceIsLeft ? -(targetNode.width / 2 + gap) : targetNode.width / 2 + gap);
337
+ ty = targetNode.y;
338
+ } else {
339
+ // Vertical or diagonal connection
340
+ sx = sourceNode.x;
341
+ sy = sourceNode.y + (sourceNode.y < targetNode.y ? sourceNode.height / 2 + gap : -(sourceNode.height / 2 + gap));
342
+ tx = targetNode.x;
343
+ ty = targetNode.y + (targetNode.y > sourceNode.y ? -(targetNode.height / 2 + gap) : targetNode.height / 2 + gap);
344
+ }
345
+
346
+ // Use curve for diagonal/horizontal connections
347
+ const midX = (sx + tx) / 2;
348
+ const midY = (sy + ty) / 2;
349
+ return `M ${sx} ${sy} C ${sx} ${midY}, ${tx} ${midY}, ${tx} ${ty}`;
350
+ })
351
+ .attr('stroke', colors.link);
352
+
353
+ // Draw label backgrounds and text (only for non-empty labels like YES/NO)
354
+ linkMerge.filter(d => d.label && d.label.trim())
355
+ .each(function (d) {
356
+ const sourceNode = nodes.find(n => n.id === d.source);
357
+ const targetNode = nodes.find(n => n.id === d.target);
358
+ const x = (sourceNode.x + targetNode.x) / 2;
359
+ const y = (sourceNode.y + targetNode.y) / 2;
360
+
361
+ const labelEl = d3.select(this);
362
+ const textEl = labelEl.select('.link-label');
363
+
364
+ // Measure text to size background (use correct font size for labels)
365
+ tempText.style('font-size', '14px').style('font-weight', '700').text(d.label);
366
+ const textWidth = tempText.node().getComputedTextLength();
367
+ const textHeight = 20;
368
+ const padding = 10;
369
+
370
+ // Position background
371
+ labelEl.select('.link-label-bg')
372
+ .attr('x', x - textWidth / 2 - padding)
373
+ .attr('y', y - textHeight / 2 - padding / 2)
374
+ .attr('width', textWidth + padding * 2)
375
+ .attr('height', textHeight + padding)
376
+ .style('opacity', 1);
377
+
378
+ // Position text
379
+ textEl
380
+ .attr('x', x)
381
+ .attr('y', y)
382
+ .text(d.label);
383
+ });
384
+
385
+ // Empty labels don't need background
386
+ linkMerge.filter(d => !d.label || !d.label.trim())
387
+ .select('.link-label')
388
+ .attr('x', d => {
389
+ const sourceNode = nodes.find(n => n.id === d.source);
390
+ const targetNode = nodes.find(n => n.id === d.target);
391
+ return (sourceNode.x + targetNode.x) / 2;
392
+ })
393
+ .attr('y', d => {
394
+ const sourceNode = nodes.find(n => n.id === d.source);
395
+ const targetNode = nodes.find(n => n.id === d.target);
396
+ return (sourceNode.y + targetNode.y) / 2;
397
+ })
398
+ .text('');
399
+
400
+ // Now remove temporary text element
401
+ tempText.remove();
402
+
403
+ // Draw nodes
404
+ const nodeGroup = gRoot.selectAll('.node-group').data(nodes);
405
+ const nodeEnter = nodeGroup.enter().append('g').attr('class', 'node-group');
406
+
407
+ nodeEnter.append('rect').attr('class', d => `node-rect node-${d.type}`);
408
+ nodeEnter.append('text').attr('class', 'node-text');
409
+
410
+ const nodeMerge = nodeEnter.merge(nodeGroup);
411
+
412
+ nodeMerge.select('.node-rect')
413
+ .attr('x', d => d.x - d.width / 2)
414
+ .attr('y', d => d.y - d.height / 2)
415
+ .attr('width', d => d.width)
416
+ .attr('height', d => d.height)
417
+ .attr('fill', d => {
418
+ // Use CSS classes for colors to support color-mix
419
+ switch (d.type) {
420
+ case 'question': return 'currentColor';
421
+ case 'decision': return colors.decision;
422
+ case 'success': return 'currentColor';
423
+ case 'category': return 'currentColor';
424
+ default: return colors.decision;
425
+ }
426
+ })
427
+ .each(function (d) {
428
+ // Set stroke based on type, using CSS variables
429
+ const strokeColor = d.type === 'decision'
430
+ ? 'var(--border-color, #ddd)'
431
+ : 'var(--border-color, #ccc)';
432
+ d3.select(this).attr('stroke', strokeColor);
433
+ })
434
+ .attr('stroke-width', 2);
435
+
436
+ nodeMerge.select('.node-text')
437
+ .attr('x', d => d.x)
438
+ .each(function (d) {
439
+ // Use wrapped lines if available, otherwise fallback to original
440
+ const lines = d.wrappedLines || d.label.split('\n');
441
+ const textEl = d3.select(this);
442
+ textEl.selectAll('tspan').remove();
443
+
444
+ const fontSize = d.type === 'category' ? '19px' : '18px';
445
+ const lineHeight = d.type === 'category' ? 28 : 26;
446
+
447
+ // Calculate vertical centering precisely
448
+ // Position text element at node center Y
449
+ textEl.attr('y', d.y);
450
+
451
+ // Calculate the offset needed to center the text block
452
+ // The first line should be positioned such that the middle of all lines aligns with d.y
453
+ const numLines = lines.length;
454
+ const totalTextHeight = (numLines - 1) * lineHeight;
455
+
456
+ // For each line, calculate its Y position relative to center
457
+ lines.forEach((line, i) => {
458
+ // Calculate offset from center: line 0 is at -totalTextHeight/2, last line at +totalTextHeight/2
459
+ const offsetFromCenter = (i - (numLines - 1) / 2) * lineHeight;
460
+
461
+ textEl.append('tspan')
462
+ .attr('x', d.x)
463
+ .attr('dy', i === 0 ? `${offsetFromCenter}px` : `${lineHeight}px`)
464
+ .attr('font-size', fontSize)
465
+ .attr('text-anchor', 'middle')
466
+ .attr('dominant-baseline', 'central')
467
+ .text(line);
468
+ });
469
+ });
470
+
471
+ // Update arrowhead color
472
+ marker.select('path').attr('fill', colors.link);
473
+ }
474
+
475
+ // Initial render + resize handling
476
+ render();
477
+ const rerender = () => render();
478
+ if (window.ResizeObserver) {
479
+ const ro = new ResizeObserver(() => rerender());
480
+ ro.observe(container);
481
+ } else {
482
+ window.addEventListener('resize', rerender);
483
+ }
484
+
485
+ // Listen for theme changes
486
+ const observer = new MutationObserver(() => {
487
+ render();
488
+ });
489
+
490
+ if (document.documentElement) {
491
+ observer.observe(document.documentElement, {
492
+ attributes: true,
493
+ attributeFilter: ['data-theme', 'class']
494
+ });
495
+ }
496
+ };
497
+
498
+ if (document.readyState === 'loading') {
499
+ document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
500
+ } else {
501
+ ensureD3(bootstrap);
502
+ }
503
+ })();
504
+ </script>
app/src/content/embeds/wrong-reasons.html CHANGED
@@ -22,12 +22,12 @@
22
  background: oklch(from var(--danger-color) calc(l + 0.4) c h / 0.26);
23
  border: 1px solid oklch(from var(--danger-color) calc(l + 0.15) c h / 0.5);
24
  border-radius: 16px;
25
- padding: var(--spacing-5);
26
- text-align: center;
27
  display: flex;
28
  flex-direction: column;
29
  justify-content: center;
30
- align-items: center;
31
  min-height: 140px;
32
  }
33
 
@@ -41,7 +41,7 @@
41
  font-size: 18px;
42
  font-weight: 600;
43
  color: var(--danger-color);
44
- margin-bottom: 0;
45
  line-height: 1.2;
46
  letter-spacing: -0.01em;
47
  position: relative;
@@ -55,20 +55,41 @@
55
 
56
  .wrong-reasons .reason-explanation {
57
  font-size: 13px;
58
- color: oklch(from var(--danger-color) calc(l + 0.05) calc(c * 0.7) h);
59
  line-height: 1.4;
60
  font-weight: 500;
61
- opacity: 0.85;
62
  position: relative;
63
  z-index: 1;
64
  font-style: italic;
65
  letter-spacing: -0.005em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
67
 
68
  /* Dark mode adjustments for better readability */
69
  [data-theme="dark"] .wrong-reasons .reason-explanation {
70
- color: oklch(from var(--danger-color) calc(l + 0.15) calc(c * 0.9) h);
71
- opacity: 0.9;
72
  }
73
 
74
  @media (max-width: 768px) {
 
22
  background: oklch(from var(--danger-color) calc(l + 0.4) c h / 0.26);
23
  border: 1px solid oklch(from var(--danger-color) calc(l + 0.15) c h / 0.5);
24
  border-radius: 16px;
25
+ padding: var(--spacing-6) var(--spacing-8);
26
+ text-align: left;
27
  display: flex;
28
  flex-direction: column;
29
  justify-content: center;
30
+ align-items: flex-start;
31
  min-height: 140px;
32
  }
33
 
 
41
  font-size: 18px;
42
  font-weight: 600;
43
  color: var(--danger-color);
44
+ margin-bottom: var(--spacing-2);
45
  line-height: 1.2;
46
  letter-spacing: -0.01em;
47
  position: relative;
 
55
 
56
  .wrong-reasons .reason-explanation {
57
  font-size: 13px;
58
+ color: oklch(from var(--danger-color) calc(l + 0.05) calc(c * 0.8) h);
59
  line-height: 1.4;
60
  font-weight: 500;
61
+ opacity: 0.9;
62
  position: relative;
63
  z-index: 1;
64
  font-style: italic;
65
  letter-spacing: -0.005em;
66
+ padding-left: 1.6em;
67
+ }
68
+
69
+ .wrong-reasons .reason-explanation::before {
70
+ content: "";
71
+ position: absolute;
72
+ left: 0;
73
+ top: 0.5em;
74
+ width: 1.4em;
75
+ height: 1.4em;
76
+ background-color: var(--danger-color);
77
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 16'%3E%3Cpath d='M2 2 L2 8 L18 8 M18 8 L14 4 M18 8 L14 12' stroke='white' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
78
+ mask-size: contain;
79
+ mask-repeat: no-repeat;
80
+ mask-position: center;
81
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 16'%3E%3Cpath d='M2 2 L2 8 L18 8 M18 8 L14 4 M18 8 L14 12' stroke='white' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
82
+ -webkit-mask-size: contain;
83
+ -webkit-mask-repeat: no-repeat;
84
+ -webkit-mask-position: center;
85
+ opacity: 0.85;
86
+ transform: translateY(-50%);
87
  }
88
 
89
  /* Dark mode adjustments for better readability */
90
  [data-theme="dark"] .wrong-reasons .reason-explanation {
91
+ color: oklch(from var(--danger-color) calc(l + 0.2) calc(c * 0.95) h);
92
+ opacity: 0.95;
93
  }
94
 
95
  @media (max-width: 768px) {