seonglae commited on
Commit
b8741fb
Β·
0 Parent(s):

Interactive research article

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .gitattributes +18 -0
  2. .gitignore +41 -0
  3. Dockerfile +35 -0
  4. LICENSE +33 -0
  5. README.md +122 -0
  6. app/astro.config.mjs +78 -0
  7. app/package-lock.json +0 -0
  8. app/package.json +0 -0
  9. app/plugins/rehype/code-copy.mjs +94 -0
  10. app/plugins/rehype/post-citation.mjs +449 -0
  11. app/plugins/rehype/restore-at-in-code.mjs +22 -0
  12. app/plugins/rehype/wrap-outputs.mjs +38 -0
  13. app/plugins/rehype/wrap-tables.mjs +43 -0
  14. app/plugins/remark/ignore-citations-in-code.mjs +21 -0
  15. app/plugins/remark/output-container.mjs +23 -0
  16. app/plugins/remark/outputs-container.mjs +23 -0
  17. app/plugins/remark/unwrap-citation-links.mjs +57 -0
  18. app/postcss.config.mjs +14 -0
  19. app/public/flowchart-svg.svg +0 -0
  20. app/public/hf-space-parent-listener.js +70 -0
  21. app/public/og.png +3 -0
  22. app/public/scripts/color-palettes.js +274 -0
  23. app/src/components/Footer.astro +347 -0
  24. app/src/components/FullWidth.astro +9 -0
  25. app/src/components/Hero.astro +612 -0
  26. app/src/components/HtmlEmbed.astro +407 -0
  27. app/src/components/Note.astro +125 -0
  28. app/src/components/Quote.astro +140 -0
  29. app/src/components/Reference.astro +45 -0
  30. app/src/components/Seo.astro +51 -0
  31. app/src/components/Sidenote.astro +99 -0
  32. app/src/components/TableOfContents.astro +860 -0
  33. app/src/components/ThemeToggle.astro +117 -0
  34. app/src/components/Wide.astro +9 -0
  35. app/src/content/article.mdx +332 -0
  36. app/src/content/assets/data/bbq-ambig-layer-sweep.json +3 -0
  37. app/src/content/assets/data/bbq-disambig-layer-sweep.json +3 -0
  38. app/src/content/assets/data/mmlu-layer-sweep-constrained.json +3 -0
  39. app/src/content/assets/data/mmlu-layer-sweep.json +3 -0
  40. app/src/content/assets/image/flowchart.png +3 -0
  41. app/src/content/bibliography.bib +1739 -0
  42. app/src/content/embeds/banner.html +389 -0
  43. app/src/content/embeds/baseline-comparison.html +201 -0
  44. app/src/content/embeds/branch-explorer.html +805 -0
  45. app/src/content/embeds/critic-trajectories.html +0 -0
  46. app/src/content/embeds/crl-flowchart-animated.html +637 -0
  47. app/src/content/embeds/feature-frequency.html +467 -0
  48. app/src/content/embeds/flowchart-svg.svg +0 -0
  49. app/src/content/embeds/layer-heatmap.html +470 -0
  50. app/src/content/embeds/norm-growth.html +568 -0
.gitattributes ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
2
+ *.jpg filter=lfs diff=lfs merge=lfs -text
3
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
4
+ *.gif filter=lfs diff=lfs merge=lfs -text
5
+ *.mp3 filter=lfs diff=lfs merge=lfs -text
6
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
7
+ *.mov filter=lfs diff=lfs merge=lfs -text
8
+ *.avi filter=lfs diff=lfs merge=lfs -text
9
+ *.wav filter=lfs diff=lfs merge=lfs -text
10
+ *.csv filter=lfs diff=lfs merge=lfs -text
11
+ *.json filter=lfs diff=lfs merge=lfs -text
12
+ *.pdf filter=lfs diff=lfs merge=lfs -text
13
+ # the package and package lock should not be tracked
14
+ package.json -filter -diff -merge text
15
+ package-lock.json -filter -diff -merge text
16
+ # Notion imported images should NOT be in LFS (needed for Docker build)
17
+ app/src/content/assets/image/image_27877f1c*.png -filter -diff -merge text
18
+ app/scripts/notion-importer/output/** -filter -diff -merge text
.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__
3
+ *.py[cod]
4
+ *.so
5
+ .Python
6
+ env/
7
+ venv/
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ *.egg
12
+ .idea/
13
+ .vscode/
14
+ .astro/
15
+ .claude/
16
+ *.swp
17
+ .DS_Store
18
+ # Node
19
+ node_modules/
20
+ *.log
21
+ *.env
22
+ *.cache
23
+ .notion-to-md
24
+
25
+ app/scripts/latex-to-mdx/output/
26
+ app/scripts/notion-importer/output/**/*
27
+ app/src/content/embeds/typography/generated
28
+
29
+ # PDF export
30
+ app/public/*.pdf
31
+ app/public/*.png
32
+ app/public/*.jpg
33
+ app/public/data/**/*
34
+
35
+ .astro/
36
+
37
+ # Template sync temporary directories
38
+ .template-sync/
39
+ .temp-*/
40
+ .backup-*/
41
+
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build Astro static site
2
+ FROM node:18-slim AS build
3
+
4
+ WORKDIR /app
5
+
6
+ COPY app/package*.json ./
7
+
8
+ RUN npm install
9
+
10
+ COPY app/ .
11
+
12
+ # Ensure public/data is a real directory (not symlink)
13
+ RUN set -e; \
14
+ rm -rf public/data; \
15
+ mkdir -p public/data; \
16
+ cp -a src/content/assets/data/. public/data/
17
+
18
+ RUN npm run build
19
+
20
+ # Stage 2: Serve with nginx
21
+ FROM nginx:alpine
22
+
23
+ COPY --from=build /app/dist /app/dist
24
+
25
+ COPY nginx.conf /etc/nginx/nginx.conf
26
+
27
+ COPY entrypoint.sh /entrypoint.sh
28
+ RUN chmod +x /entrypoint.sh
29
+
30
+ RUN mkdir -p /var/cache/nginx /var/run /var/log/nginx /var/lib/nginx/body /tmp && \
31
+ chmod -R 777 /var/cache/nginx /var/run /var/log/nginx /var/lib/nginx /tmp /app
32
+
33
+ EXPOSE 7860
34
+
35
+ ENTRYPOINT ["/entrypoint.sh"]
LICENSE ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Creative Commons Attribution 4.0 International License
2
+
3
+ Copyright (c) 2024 Thibaud Frere
4
+
5
+ This work is licensed under the Creative Commons Attribution 4.0 International License.
6
+ To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/
7
+ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
8
+
9
+ You are free to:
10
+
11
+ Share β€” copy and redistribute the material in any medium or format
12
+ Adapt β€” remix, transform, and build upon the material for any purpose, even commercially.
13
+
14
+ The licensor cannot revoke these freedoms as long as you follow the license terms.
15
+
16
+ Under the following terms:
17
+
18
+ Attribution β€” You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
19
+
20
+ No additional restrictions β€” You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
21
+
22
+ Notices:
23
+
24
+ You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation.
25
+
26
+ No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.
27
+
28
+ ---
29
+
30
+ For the source code and technical implementation:
31
+ - The source code is available at: https://huggingface.co/spaces/tfrere/research-article-template
32
+ - Third-party figures and assets are excluded from this license and marked in their captions
33
+ - Dependencies and third-party libraries maintain their respective licenses
README.md ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: 'Control Reinforcement Learning'
3
+ short_desc: 'Interpretable Token-Level Steering of LLMs via SAE Features'
4
+ emoji: πŸŽ›οΈ
5
+ colorFrom: indigo
6
+ colorTo: purple
7
+ sdk: docker
8
+ pinned: false
9
+ header: mini
10
+ app_port: 7860
11
+ tags:
12
+ - research-article-template
13
+ - research paper
14
+ - scientific paper
15
+ - data visualization
16
+ thumbnail: https://huggingface.co/spaces/tfrere/research-paper-template/thumb.jpg
17
+ ---
18
+ <div align="center">
19
+
20
+ # Research Article Template
21
+
22
+ [![License: CC BY 4.0](https://img.shields.io/badge/License-CC%20BY%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by/4.0/)
23
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg)](https://nodejs.org/)
24
+ [![Astro](https://img.shields.io/badge/Astro-4.10.0-orange.svg)](https://astro.build/)
25
+ [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/tfrere/research-article-template)
26
+
27
+
28
+ **A modern, interactive template for scientific writing** that brings papers to life with web-native features. The web offers what static PDFs can't: **interactive diagrams**, **progressive notation**, and **exploratory views** that show how ideas behave. This template treats interactive artifactsβ€”figures, math, code, and inspectable experimentsβ€”as **first-class** alongside prose, helping readers **build intuition** instead of skimming resultsβ€”all with **minimal setup** and no web knowledge required.
29
+
30
+ **[Try the live demo & documentation β†’](https://huggingface.co/spaces/tfrere/research-article-template)**
31
+
32
+ </div>
33
+
34
+ ## πŸš€ Quick Start
35
+
36
+ ### Option 1: Duplicate on Hugging Face (Recommended)
37
+
38
+ 1. Visit **[πŸ€— Research Article Template](https://huggingface.co/spaces/tfrere/research-article-template)**
39
+ 2. Click **"Duplicate this Space"**
40
+ 3. Clone your new repository:
41
+ ```bash
42
+ git clone git@hf.co:spaces/<your-username>/<your-space>
43
+ cd <your-space>
44
+ ```
45
+
46
+ ### Option 2: Clone Directly
47
+
48
+ ```bash
49
+ git clone https://github.com/tfrere/research-article-template.git
50
+ cd research-article-template
51
+ ```
52
+
53
+ ### Installation
54
+
55
+ ```bash
56
+ # Install Node.js 20+ (use nvm for version management)
57
+ nvm install 20
58
+ nvm use 20
59
+
60
+ # Install Git LFS and pull assets
61
+ git lfs install
62
+ git lfs pull
63
+
64
+ # Install dependencies
65
+ cd app
66
+ npm install
67
+
68
+ # Start development server
69
+ npm run dev
70
+ ```
71
+
72
+ Visit `http://localhost:4321` to see your site!
73
+
74
+ ## 🎯 Who This Is For
75
+
76
+ - **Scientists** writing modern, web-native research papers
77
+ - **Educators** creating interactive, explorable lessons
78
+ - **Researchers** who want to focus on ideas, not infrastructure
79
+ - **Anyone** who values clear, engaging technical communication
80
+
81
+ ## 🌟 Inspired by Distill
82
+
83
+ This template carries forward the spirit of [Distill](https://distill.pub/) (2016–2021), pushing interactive scientific writing even further with:
84
+ - Accessible, high-quality explanations
85
+ - Reproducible, production-ready demos
86
+ - Modern web technologies and best practices
87
+
88
+ ## 🀝 Contributing
89
+
90
+ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details.
91
+
92
+ ### Ways to Contribute
93
+
94
+ - **Report bugs** - Open an issue with detailed information
95
+ - **Suggest features** - Share ideas for improvements
96
+ - **Improve documentation** - Help others get started
97
+ - **Submit code** - Fix bugs or add features
98
+ - **Join discussions** - Share feedback and ideas
99
+
100
+ ## πŸ“„ License
101
+
102
+ This project is licensed under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/).
103
+
104
+ - **Diagrams and text**: CC-BY 4.0
105
+ - **Source code**: Available on [Hugging Face](https://huggingface.co/spaces/tfrere/research-article-template)
106
+ - **Third-party figures**: Excluded and marked in captions
107
+
108
+ ## πŸ™ Acknowledgments
109
+
110
+ - Inspired by [Distill](https://distill.pub/) and the interactive scientific writing movement
111
+ - Built with [Astro](https://astro.build/), [MDX](https://mdxjs.com/), and modern web technologies
112
+ - Community feedback and contributions from researchers worldwide
113
+
114
+ ## πŸ“ž Support
115
+
116
+ - **[Community Discussions](https://huggingface.co/spaces/tfrere/research-article-template/discussions)** - Ask questions and share ideas
117
+ - **[Report Issues](https://huggingface.co/spaces/tfrere/research-article-template/discussions?status=open&type=issue)** - Bug reports and feature requests
118
+ - **Contact**: [@tfrere](https://huggingface.co/tfrere) on Hugging Face
119
+
120
+ ---
121
+
122
+ **Made with ❀️ for the scientific community**
app/astro.config.mjs ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'astro/config';
2
+ import mdx from '@astrojs/mdx';
3
+ import svelte from '@astrojs/svelte';
4
+ import compressor from 'astro-compressor';
5
+ import remarkMath from 'remark-math';
6
+ import rehypeKatex from 'rehype-katex';
7
+ import remarkFootnotes from 'remark-footnotes';
8
+ import rehypeSlug from 'rehype-slug';
9
+ import rehypeAutolinkHeadings from 'rehype-autolink-headings';
10
+ import rehypeCodeCopy from './plugins/rehype/code-copy.mjs';
11
+ import rehypeCitation from 'rehype-citation';
12
+ import rehypeReferencesAndFootnotes from './plugins/rehype/post-citation.mjs';
13
+ import remarkIgnoreCitationsInCode from './plugins/remark/ignore-citations-in-code.mjs';
14
+ import remarkUnwrapCitationLinks from './plugins/remark/unwrap-citation-links.mjs';
15
+ import remarkDirective from 'remark-directive';
16
+ import remarkOutputContainer from './plugins/remark/output-container.mjs';
17
+ import rehypeRestoreAtInCode from './plugins/rehype/restore-at-in-code.mjs';
18
+ import rehypeWrapTables from './plugins/rehype/wrap-tables.mjs';
19
+ import rehypeWrapOutput from './plugins/rehype/wrap-outputs.mjs';
20
+ // Built-in Shiki (dual themes) β€” no rehype-pretty-code
21
+
22
+ // Plugins moved to app/plugins/*
23
+
24
+ export default defineConfig({
25
+ output: 'static',
26
+ integrations: [
27
+ mdx(),
28
+ svelte(),
29
+ // Precompress output with Gzip only (Brotli disabled due to server module mismatch)
30
+ compressor({ brotli: false, gzip: true })
31
+ ],
32
+ devToolbar: {
33
+ enabled: false
34
+ },
35
+ markdown: {
36
+ shikiConfig: {
37
+ themes: {
38
+ light: 'github-light',
39
+ dark: 'github-dark'
40
+ },
41
+ defaultColor: false,
42
+ wrap: false,
43
+ langAlias: {
44
+ // Map MDX fences to TSX for better JSX tokenization
45
+ mdx: 'tsx'
46
+ }
47
+ },
48
+ remarkPlugins: [
49
+ remarkUnwrapCitationLinks,
50
+ remarkIgnoreCitationsInCode,
51
+ remarkMath,
52
+ [remarkFootnotes, { inlineNotes: true }],
53
+ remarkDirective,
54
+ remarkOutputContainer
55
+ ],
56
+ rehypePlugins: [
57
+ rehypeSlug,
58
+ [rehypeAutolinkHeadings, { behavior: 'wrap' }],
59
+ [rehypeKatex, {
60
+ trust: true,
61
+ }],
62
+ [rehypeCitation, {
63
+ bibliography: 'src/content/bibliography.bib',
64
+ linkCitations: true,
65
+ csl: "apa",
66
+ noCite: false,
67
+ suppressBibliography: false,
68
+ }],
69
+ rehypeReferencesAndFootnotes,
70
+ rehypeRestoreAtInCode,
71
+ rehypeCodeCopy,
72
+ rehypeWrapOutput,
73
+ rehypeWrapTables
74
+ ]
75
+ }
76
+ });
77
+
78
+
app/package-lock.json ADDED
Binary file (368 kB). View file
 
app/package.json ADDED
Binary file (945 Bytes). View file
 
app/plugins/rehype/code-copy.mjs ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Minimal rehype plugin to wrap code blocks with a copy button
2
+ // Exported as a standalone module to keep astro.config.mjs lean
3
+ export default function rehypeCodeCopy() {
4
+ return (tree) => {
5
+ // Walk the tree; lightweight visitor to find <pre><code>
6
+ const visit = (node, parent) => {
7
+ if (!node || typeof node !== 'object') return;
8
+ const children = Array.isArray(node.children) ? node.children : [];
9
+ if (node.tagName === 'pre' && children.some(c => c.tagName === 'code')) {
10
+ // Find code child
11
+ const code = children.find(c => c.tagName === 'code');
12
+ // Determine if single-line block: prefer Shiki lines, then text content
13
+ const countLinesFromShiki = () => {
14
+ const isLineEl = (el) => el && el.type === 'element' && el.tagName === 'span' && Array.isArray(el.properties?.className) && el.properties.className.includes('line');
15
+ const hasNonWhitespaceText = (node) => {
16
+ if (!node) return false;
17
+ if (node.type === 'text') return /\S/.test(String(node.value || ''));
18
+ const kids = Array.isArray(node.children) ? node.children : [];
19
+ return kids.some(hasNonWhitespaceText);
20
+ };
21
+ const collectLines = (node, acc) => {
22
+ if (!node || typeof node !== 'object') return;
23
+ if (isLineEl(node)) acc.push(node);
24
+ const kids = Array.isArray(node.children) ? node.children : [];
25
+ kids.forEach((k) => collectLines(k, acc));
26
+ };
27
+ const lines = [];
28
+ collectLines(code, lines);
29
+ const nonEmpty = lines.filter((ln) => hasNonWhitespaceText(ln)).length;
30
+ return nonEmpty || 0;
31
+ };
32
+ const countLinesFromText = () => {
33
+ // Parse raw text content of the <code> node including nested spans
34
+ const extractText = (node) => {
35
+ if (!node) return '';
36
+ if (node.type === 'text') return String(node.value || '');
37
+ const kids = Array.isArray(node.children) ? node.children : [];
38
+ return kids.map(extractText).join('');
39
+ };
40
+ const raw = extractText(code);
41
+ if (!raw || !/\S/.test(raw)) return 0;
42
+ return raw.split('\n').filter(line => /\S/.test(line)).length;
43
+ };
44
+ const lines = countLinesFromShiki() || countLinesFromText();
45
+ const isSingleLine = lines <= 1;
46
+ // Also treat code blocks shorter than a threshold as single-line (defensive)
47
+ if (!isSingleLine) {
48
+ const approxChars = (() => {
49
+ const extract = (n) => Array.isArray(n?.children) ? n.children.map(extract).join('') : (n?.type === 'text' ? String(n.value||'') : '');
50
+ return extract(code).length;
51
+ })();
52
+ if (approxChars < 6) {
53
+ node.__forceSingle = true;
54
+ }
55
+ }
56
+ // Replace <pre> with wrapper div.code-card containing button + pre
57
+ const wrapper = {
58
+ type: 'element',
59
+ tagName: 'div',
60
+ properties: { className: ['code-card'].concat((isSingleLine || node.__forceSingle) ? ['no-copy'] : []) },
61
+ children: (isSingleLine || node.__forceSingle) ? [ node ] : [
62
+ {
63
+ type: 'element',
64
+ tagName: 'button',
65
+ properties: { className: ['code-copy', 'button--ghost'], type: 'button', 'aria-label': 'Copy code' },
66
+ children: [
67
+ {
68
+ type: 'element',
69
+ tagName: 'svg',
70
+ properties: { viewBox: '0 0 24 24', 'aria-hidden': 'true', focusable: 'false' },
71
+ children: [
72
+ { type: 'element', tagName: 'path', properties: { d: 'M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z' }, children: [] }
73
+ ]
74
+ }
75
+ ]
76
+ },
77
+ node
78
+ ]
79
+ };
80
+ if (parent && Array.isArray(parent.children)) {
81
+ const idx = parent.children.indexOf(node);
82
+ if (idx !== -1) parent.children[idx] = wrapper;
83
+ }
84
+ return; // don't visit nested
85
+ }
86
+ children.forEach((c) => visit(c, node));
87
+ };
88
+ visit(tree, null);
89
+ };
90
+ }
91
+
92
+
93
+
94
+
app/plugins/rehype/post-citation.mjs ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // rehype plugin to post-process citations and footnotes at build-time
2
+ // - Normalizes the bibliography into <ol class="references"> with <li id="...">
3
+ // - Linkifies DOI/URL occurrences inside references
4
+ // - Appends back-reference links (↩ back: 1, 2, ...) from each reference to in-text citation anchors
5
+ // - Cleans up footnotes block (.footnotes)
6
+
7
+ export default function rehypeReferencesAndFootnotes() {
8
+ return (tree) => {
9
+ const isElement = (n) => n && typeof n === 'object' && n.type === 'element';
10
+ const getChildren = (n) => (Array.isArray(n?.children) ? n.children : []);
11
+
12
+ const walk = (node, parent, fn) => {
13
+ if (!node || typeof node !== 'object') return;
14
+ fn && fn(node, parent);
15
+ const kids = getChildren(node);
16
+ for (const child of kids) walk(child, node, fn);
17
+ };
18
+
19
+ const ensureArray = (v) => (Array.isArray(v) ? v : v != null ? [v] : []);
20
+
21
+ const hasClass = (el, name) => {
22
+ const cn = ensureArray(el?.properties?.className).map(String);
23
+ return cn.includes(name);
24
+ };
25
+
26
+ const setAttr = (el, key, val) => {
27
+ el.properties = el.properties || {};
28
+ if (val == null) delete el.properties[key];
29
+ else el.properties[key] = val;
30
+ };
31
+
32
+ const getAttr = (el, key) => (el?.properties ? el.properties[key] : undefined);
33
+
34
+ // Shared helpers for backlinks + backrefs block
35
+ const collectBacklinksForIdSet = (idSet, anchorPrefix) => {
36
+ const idToBacklinks = new Map();
37
+ const idToAnchorNodes = new Map();
38
+ if (!idSet || idSet.size === 0) return { idToBacklinks, idToAnchorNodes };
39
+ walk(tree, null, (node) => {
40
+ if (!isElement(node) || node.tagName !== 'a') return;
41
+ const href = String(getAttr(node, 'href') || '');
42
+ if (!href.startsWith('#')) return;
43
+ const id = href.slice(1);
44
+ if (!idSet.has(id)) return;
45
+ // Ensure a stable id
46
+ let anchorId = String(getAttr(node, 'id') || '');
47
+ if (!anchorId) {
48
+ const list = idToBacklinks.get(id) || [];
49
+ anchorId = `${anchorPrefix}-${id}-${list.length + 1}`;
50
+ setAttr(node, 'id', anchorId);
51
+ }
52
+ const list = idToBacklinks.get(id) || [];
53
+ list.push(anchorId);
54
+ idToBacklinks.set(id, list);
55
+ const nodes = idToAnchorNodes.get(id) || [];
56
+ nodes.push(node);
57
+ idToAnchorNodes.set(id, nodes);
58
+ });
59
+ return { idToBacklinks, idToAnchorNodes };
60
+ };
61
+
62
+ const createBackIcon = () => ({
63
+ type: 'element',
64
+ tagName: 'svg',
65
+ properties: {
66
+ className: ['back-icon'],
67
+ width: 12,
68
+ height: 12,
69
+ viewBox: '0 0 24 24',
70
+ fill: 'none',
71
+ stroke: 'currentColor',
72
+ 'stroke-width': 2,
73
+ 'stroke-linecap': 'round',
74
+ 'stroke-linejoin': 'round',
75
+ 'aria-hidden': 'true',
76
+ focusable: 'false'
77
+ },
78
+ children: [
79
+ { type: 'element', tagName: 'line', properties: { x1: 12, y1: 19, x2: 12, y2: 5 }, children: [] },
80
+ { type: 'element', tagName: 'polyline', properties: { points: '5 12 12 5 19 12' }, children: [] }
81
+ ]
82
+ });
83
+
84
+ const appendBackrefsBlock = (listElement, idToBacklinks, ariaLabel) => {
85
+ if (!listElement || !idToBacklinks || idToBacklinks.size === 0) return;
86
+ for (const li of getChildren(listElement)) {
87
+ if (!isElement(li) || li.tagName !== 'li') continue;
88
+ const id = String(getAttr(li, 'id') || '');
89
+ if (!id) continue;
90
+ const keys = idToBacklinks.get(id);
91
+ if (!keys || !keys.length) continue;
92
+ // Remove pre-existing .backrefs in this li to avoid duplicates
93
+ li.children = getChildren(li).filter((n) => !(isElement(n) && n.tagName === 'small' && hasClass(n, 'backrefs')));
94
+ const small = {
95
+ type: 'element',
96
+ tagName: 'small',
97
+ properties: { className: ['backrefs'] },
98
+ children: []
99
+ };
100
+ if (keys.length === 1) {
101
+ // Single backlink: just the icon wrapped in the anchor
102
+ const a = {
103
+ type: 'element',
104
+ tagName: 'a',
105
+ properties: { href: `#${keys[0]}`, 'aria-label': ariaLabel },
106
+ children: [createBackIcon()]
107
+ };
108
+ small.children.push(a);
109
+ } else {
110
+ // Multiple backlinks: icon + label + numbered links
111
+ small.children.push(createBackIcon());
112
+ small.children.push({ type: 'text', value: ' back: ' });
113
+ keys.forEach((backId, idx) => {
114
+ small.children.push({
115
+ type: 'element',
116
+ tagName: 'a',
117
+ properties: { href: `#${backId}`, 'aria-label': ariaLabel },
118
+ children: [{ type: 'text', value: String(idx + 1) }]
119
+ });
120
+ if (idx < keys.length - 1) small.children.push({ type: 'text', value: ', ' });
121
+ });
122
+ }
123
+ li.children.push(small);
124
+ }
125
+ };
126
+ // Remove default back-reference anchors generated by remark-footnotes inside a footnote item
127
+ const getTextContent = (el) => {
128
+ if (!el) return '';
129
+ const stack = [el];
130
+ let out = '';
131
+ while (stack.length) {
132
+ const cur = stack.pop();
133
+ if (!cur) continue;
134
+ if (cur.type === 'text') out += String(cur.value || '');
135
+ const kids = getChildren(cur);
136
+ for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
137
+ }
138
+ return out;
139
+ };
140
+
141
+ const removeFootnoteBackrefAnchors = (el) => {
142
+ if (!isElement(el)) return;
143
+ const kids = getChildren(el);
144
+ for (let i = kids.length - 1; i >= 0; i--) {
145
+ const child = kids[i];
146
+ if (isElement(child)) {
147
+ if (
148
+ child.tagName === 'a' && (
149
+ getAttr(child, 'data-footnote-backref') != null ||
150
+ hasClass(child, 'footnote-backref') ||
151
+ String(getAttr(child, 'role') || '').toLowerCase() === 'doc-backlink' ||
152
+ String(getAttr(child, 'aria-label') || '').toLowerCase().includes('back to content') ||
153
+ String(getAttr(child, 'href') || '').startsWith('#fnref') ||
154
+ // Fallback: text-based detection like "↩" or "↩2"
155
+ /^\s*↩\s*\d*\s*$/u.test(getTextContent(child))
156
+ )
157
+ ) {
158
+ // Remove the anchor
159
+ el.children.splice(i, 1);
160
+ continue;
161
+ }
162
+ // Recurse into element
163
+ removeFootnoteBackrefAnchors(child);
164
+ // If a wrapper like <sup> or <span> became empty, remove it
165
+ const becameKids = getChildren(child);
166
+ if ((child.tagName === 'sup' || child.tagName === 'span') && (!becameKids || becameKids.length === 0)) {
167
+ el.children.splice(i, 1);
168
+ }
169
+ }
170
+ }
171
+ };
172
+
173
+
174
+ const normDoiHref = (href) => {
175
+ if (!href) return href;
176
+ const DUP = /https?:\/\/(?:dx\.)?doi\.org\/(?:https?:\/\/(?:dx\.)?doi\.org\/)+/gi;
177
+ const ONE = /https?:\/\/(?:dx\.)?doi\.org\/(10\.[^\s<>"']+)/i;
178
+ href = String(href).replace(DUP, 'https://doi.org/');
179
+ const m = href.match(ONE);
180
+ return m ? `https://doi.org/${m[1]}` : href;
181
+ };
182
+
183
+ const DOI_BARE = /\b10\.[0-9]{4,9}\/[\-._;()\/:A-Z0-9]+\b/gi;
184
+ const URL_GEN = /\bhttps?:\/\/[^\s<>()"']+/gi;
185
+
186
+ const linkifyTextNode = (textNode) => {
187
+ const text = String(textNode.value || '');
188
+ let last = 0;
189
+ const parts = [];
190
+ const pushText = (s) => { if (s) parts.push({ type: 'text', value: s }); };
191
+
192
+ const matches = [];
193
+ // Collect URL matches
194
+ let m;
195
+ URL_GEN.lastIndex = 0;
196
+ while ((m = URL_GEN.exec(text)) !== null) {
197
+ matches.push({ type: 'url', start: m.index, end: URL_GEN.lastIndex, raw: m[0] });
198
+ }
199
+ // Collect DOI matches
200
+ DOI_BARE.lastIndex = 0;
201
+ while ((m = DOI_BARE.exec(text)) !== null) {
202
+ matches.push({ type: 'doi', start: m.index, end: DOI_BARE.lastIndex, raw: m[0] });
203
+ }
204
+ matches.sort((a, b) => a.start - b.start);
205
+
206
+ for (const match of matches) {
207
+ if (match.start < last) continue; // overlapping
208
+ pushText(text.slice(last, match.start));
209
+ if (match.type === 'url') {
210
+ const href = normDoiHref(match.raw);
211
+ const doiOne = href.match(/https?:\/\/(?:dx\.)?doi\.org\/(10\.[^\s<>"']+)/i);
212
+ const a = {
213
+ type: 'element',
214
+ tagName: 'a',
215
+ properties: { href, target: '_blank', rel: 'noopener noreferrer' },
216
+ children: [{ type: 'text', value: doiOne ? doiOne[1] : href }]
217
+ };
218
+ parts.push(a);
219
+ } else {
220
+ const href = `https://doi.org/${match.raw}`;
221
+ const a = {
222
+ type: 'element',
223
+ tagName: 'a',
224
+ properties: { href, target: '_blank', rel: 'noopener noreferrer' },
225
+ children: [{ type: 'text', value: match.raw }]
226
+ };
227
+ parts.push(a);
228
+ }
229
+ last = match.end;
230
+ }
231
+
232
+ pushText(text.slice(last));
233
+ return parts;
234
+ };
235
+
236
+ const linkifyInElement = (el) => {
237
+ const kids = getChildren(el);
238
+ for (let i = 0; i < kids.length; i++) {
239
+ const child = kids[i];
240
+ if (!child) continue;
241
+ if (child.type === 'text') {
242
+ const replacement = linkifyTextNode(child);
243
+ if (replacement.length === 1 && replacement[0].type === 'text') continue;
244
+ // Replace the single text node with multiple nodes
245
+ el.children.splice(i, 1, ...replacement);
246
+ i += replacement.length - 1;
247
+ } else if (isElement(child)) {
248
+ if (child.tagName === 'a') {
249
+ const href = normDoiHref(getAttr(child, 'href'));
250
+ setAttr(child, 'href', href);
251
+ const m = String(href || '').match(/https?:\/\/(?:dx\.)?doi\.org\/(10\.[^\s<>"']+)/i);
252
+ if (m && (!child.children || child.children.length === 0)) {
253
+ child.children = [{ type: 'text', value: m[1] }];
254
+ }
255
+ continue;
256
+ }
257
+ linkifyInElement(child);
258
+ }
259
+ }
260
+ // Deduplicate adjacent identical anchors
261
+ for (let i = 1; i < el.children.length; i++) {
262
+ const prev = el.children[i - 1];
263
+ const curr = el.children[i];
264
+ if (isElement(prev) && isElement(curr) && prev.tagName === 'a' && curr.tagName === 'a') {
265
+ const key = `${getAttr(prev, 'href') || ''}|${(prev.children?.[0]?.value) || ''}`;
266
+ const key2 = `${getAttr(curr, 'href') || ''}|${(curr.children?.[0]?.value) || ''}`;
267
+ if (key === key2) {
268
+ el.children.splice(i, 1);
269
+ i--;
270
+ }
271
+ }
272
+ }
273
+ };
274
+
275
+ // Find references container and normalize its list
276
+ const findReferencesRoot = () => {
277
+ let found = null;
278
+ walk(tree, null, (node) => {
279
+ if (found) return;
280
+ if (!isElement(node)) return;
281
+
282
+ // Ignore headers (h1, h2, h3, h4, h5, h6) - we only want container elements
283
+ if (/^h[1-6]$/i.test(node.tagName)) return;
284
+
285
+ const id = getAttr(node, 'id');
286
+ if (id === 'references' || id === 'refs' || hasClass(node, 'references') || hasClass(node, 'bibliography')) {
287
+ found = node;
288
+ }
289
+ });
290
+ return found;
291
+ };
292
+
293
+ const toOrderedList = (container) => {
294
+ // If there is already an <ol>, use it; otherwise convert common structures
295
+ let ol = getChildren(container).find((c) => isElement(c) && c.tagName === 'ol');
296
+ if (!ol) {
297
+ ol = { type: 'element', tagName: 'ol', properties: { className: ['references'] }, children: [] };
298
+ const candidates = getChildren(container).filter((n) => isElement(n));
299
+ if (candidates.length) {
300
+ for (const node of candidates) {
301
+ if (hasClass(node, 'csl-entry') || node.tagName === 'li' || node.tagName === 'p' || node.tagName === 'div') {
302
+ const li = { type: 'element', tagName: 'li', properties: {}, children: getChildren(node) };
303
+ if (getAttr(node, 'id')) setAttr(li, 'id', getAttr(node, 'id'));
304
+ ol.children.push(li);
305
+ }
306
+ }
307
+ }
308
+ // Replace container children by the new ol
309
+ container.children = [ol];
310
+ }
311
+ if (!hasClass(ol, 'references')) {
312
+ const cls = ensureArray(ol.properties?.className).map(String);
313
+ if (!cls.includes('references')) cls.push('references');
314
+ ol.properties = ol.properties || {};
315
+ ol.properties.className = cls;
316
+ }
317
+ return ol;
318
+ };
319
+
320
+ const refsRoot = findReferencesRoot();
321
+ let refsOl = null;
322
+ const refIdSet = new Set();
323
+ const refIdToExternalHref = new Map();
324
+
325
+ if (refsRoot) {
326
+ // Add a unique id to avoid collisions with user-created headers
327
+ setAttr(refsRoot, 'id', 'bibliography-references-list');
328
+ setAttr(refsRoot, 'data-bibliography-block', 'true');
329
+
330
+ refsOl = toOrderedList(refsRoot);
331
+ // Collect item ids and linkify their content
332
+ for (const li of getChildren(refsOl)) {
333
+ if (!isElement(li) || li.tagName !== 'li') continue;
334
+ if (!getAttr(li, 'id')) {
335
+ // Try to find a nested element with id to promote
336
+ const nestedWithId = getChildren(li).find((n) => isElement(n) && getAttr(n, 'id'));
337
+ if (nestedWithId) setAttr(li, 'id', getAttr(nestedWithId, 'id'));
338
+ }
339
+ const id = getAttr(li, 'id');
340
+ if (id) refIdSet.add(String(id));
341
+ linkifyInElement(li);
342
+ // Record first external link href (e.g., DOI/URL) if present
343
+ if (id) {
344
+ let externalHref = null;
345
+ const stack = [li];
346
+ while (stack.length) {
347
+ const cur = stack.pop();
348
+ const kids = getChildren(cur);
349
+ for (const k of kids) {
350
+ if (isElement(k) && k.tagName === 'a') {
351
+ const href = String(getAttr(k, 'href') || '');
352
+ if (/^https?:\/\//i.test(href)) {
353
+ externalHref = href;
354
+ break;
355
+ }
356
+ }
357
+ if (isElement(k)) stack.push(k);
358
+ }
359
+ if (externalHref) break;
360
+ }
361
+ if (externalHref) refIdToExternalHref.set(String(id), externalHref);
362
+ }
363
+ }
364
+ setAttr(refsRoot, 'data-built-refs', '1');
365
+ }
366
+
367
+ // Collect in-text anchors that point to references ids
368
+ const { idToBacklinks: refIdToBacklinks, idToAnchorNodes: refIdToCitationAnchors } = collectBacklinksForIdSet(refIdSet, 'refctx');
369
+
370
+ // Append backlinks into references list items
371
+ appendBackrefsBlock(refsOl, refIdToBacklinks, 'Back to citation');
372
+
373
+ // Rewrite in-text citation anchors to external link when available
374
+ if (refIdToCitationAnchors.size > 0) {
375
+ for (const [id, anchors] of refIdToCitationAnchors.entries()) {
376
+ const ext = refIdToExternalHref.get(id);
377
+ if (!ext) continue;
378
+ for (const a of anchors) {
379
+ setAttr(a, 'data-ref-id', id);
380
+ setAttr(a, 'href', ext);
381
+ const existingTarget = getAttr(a, 'target');
382
+ if (!existingTarget) setAttr(a, 'target', '_blank');
383
+ const rel = String(getAttr(a, 'rel') || '');
384
+ const relSet = new Set(rel ? rel.split(/\s+/) : []);
385
+ relSet.add('noopener');
386
+ relSet.add('noreferrer');
387
+ setAttr(a, 'rel', Array.from(relSet).join(' '));
388
+ }
389
+ }
390
+ }
391
+
392
+ // Footnotes cleanup + backrefs harmonized with references
393
+ const cleanupFootnotes = () => {
394
+ let root = null;
395
+ walk(tree, null, (node) => {
396
+ if (!isElement(node)) return;
397
+ if (hasClass(node, 'footnotes')) root = node;
398
+ });
399
+ if (!root) return { root: null, ol: null, idSet: new Set() };
400
+ // Remove <hr> direct children
401
+ root.children = getChildren(root).filter((n) => !(isElement(n) && n.tagName === 'hr'));
402
+ // Ensure an <ol>
403
+ let ol = getChildren(root).find((c) => isElement(c) && c.tagName === 'ol');
404
+ if (!ol) {
405
+ ol = { type: 'element', tagName: 'ol', properties: {}, children: [] };
406
+ const items = getChildren(root).filter((n) => isElement(n) && (n.tagName === 'li' || hasClass(n, 'footnote') || n.tagName === 'p' || n.tagName === 'div'));
407
+ if (items.length) {
408
+ for (const it of items) {
409
+ const li = { type: 'element', tagName: 'li', properties: {}, children: getChildren(it) };
410
+ // Promote nested id if present (e.g., <p id="fn-1">)
411
+ const nestedWithId = getChildren(it).find((n) => isElement(n) && getAttr(n, 'id'));
412
+ if (nestedWithId) setAttr(li, 'id', getAttr(nestedWithId, 'id'));
413
+ ol.children.push(li);
414
+ }
415
+ }
416
+ root.children = [ol];
417
+ }
418
+ // For existing structures, try to promote ids from children when missing
419
+ for (const li of getChildren(ol)) {
420
+ if (!isElement(li) || li.tagName !== 'li') continue;
421
+ if (!getAttr(li, 'id')) {
422
+ const nestedWithId = getChildren(li).find((n) => isElement(n) && getAttr(n, 'id'));
423
+ if (nestedWithId) setAttr(li, 'id', getAttr(nestedWithId, 'id'));
424
+ }
425
+ // Remove default footnote backrefs anywhere inside (to avoid duplication)
426
+ removeFootnoteBackrefAnchors(li);
427
+ }
428
+ setAttr(root, 'data-built-footnotes', '1');
429
+ // Collect id set
430
+ const idSet = new Set();
431
+ for (const li of getChildren(ol)) {
432
+ if (!isElement(li) || li.tagName !== 'li') continue;
433
+ const id = getAttr(li, 'id');
434
+ if (id) idSet.add(String(id));
435
+ }
436
+ return { root, ol, idSet };
437
+ };
438
+
439
+ const { root: footRoot, ol: footOl, idSet: footIdSet } = cleanupFootnotes();
440
+
441
+ // Collect in-text anchors pointing to footnotes
442
+ const { idToBacklinks: footIdToBacklinks } = collectBacklinksForIdSet(footIdSet, 'footctx');
443
+
444
+ // Append backlinks into footnote list items (identical pattern to references)
445
+ appendBackrefsBlock(footOl, footIdToBacklinks, 'Back to footnote call');
446
+ };
447
+ }
448
+
449
+
app/plugins/rehype/restore-at-in-code.mjs ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Rehype plugin to restore '@' inside code nodes after rehype-citation ran
2
+ export default function rehypeRestoreAtInCode() {
3
+ return (tree) => {
4
+ const restoreInNode = (node) => {
5
+ if (!node || typeof node !== 'object') return;
6
+ const isText = node.type === 'text';
7
+ if (isText && typeof node.value === 'string' && node.value.includes('__AT_SENTINEL__')) {
8
+ node.value = node.value.replace(/__AT_SENTINEL__/g, '@');
9
+ }
10
+ const isCodeEl = node.type === 'element' && node.tagName === 'code';
11
+ const children = Array.isArray(node.children) ? node.children : [];
12
+ if (isCodeEl && children.length) {
13
+ children.forEach(restoreInNode);
14
+ return;
15
+ }
16
+ children.forEach(restoreInNode);
17
+ };
18
+ restoreInNode(tree);
19
+ };
20
+ }
21
+
22
+
app/plugins/rehype/wrap-outputs.mjs ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Wrap plain-text content inside <section class="code-outputs"> into a <pre>
2
+ export default function rehypeWrapOutput() {
3
+ return (tree) => {
4
+ const isWhitespace = (value) => typeof value === 'string' && !/\S/.test(value);
5
+ const extractText = (node) => {
6
+ if (!node) return '';
7
+ if (node.type === 'text') return String(node.value || '');
8
+ const kids = Array.isArray(node.children) ? node.children : [];
9
+ return kids.map(extractText).join('');
10
+ };
11
+ const visit = (node) => {
12
+ if (!node || typeof node !== 'object') return;
13
+ const children = Array.isArray(node.children) ? node.children : [];
14
+ if (node.type === 'element' && node.tagName === 'section') {
15
+ const className = node.properties?.className || [];
16
+ const classes = Array.isArray(className) ? className : [className].filter(Boolean);
17
+ if (classes.includes('code-output')) {
18
+ const meaningful = children.filter((c) => !(c.type === 'text' && isWhitespace(c.value)));
19
+ if (meaningful.length === 1) {
20
+ const only = meaningful[0];
21
+ const isPlainParagraph = only.type === 'element' && only.tagName === 'p' && (only.children || []).every((c) => c.type === 'text');
22
+ const isPlainText = only.type === 'text';
23
+ if (isPlainParagraph || isPlainText) {
24
+ const text = isPlainText ? String(only.value || '') : extractText(only);
25
+ node.children = [
26
+ { type: 'element', tagName: 'pre', properties: {}, children: [ { type: 'text', value: text } ] }
27
+ ];
28
+ }
29
+ }
30
+ }
31
+ }
32
+ children.forEach(visit);
33
+ };
34
+ visit(tree);
35
+ };
36
+ }
37
+
38
+
app/plugins/rehype/wrap-tables.mjs ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // rehype plugin: wrap bare <table> elements in a <div class="table-scroll"> container
2
+ // so that tables stay width:100% while enabling horizontal scroll when content overflows
3
+
4
+ export default function rehypeWrapTables() {
5
+ return (tree) => {
6
+ const isElement = (n) => n && typeof n === 'object' && n.type === 'element';
7
+ const getChildren = (n) => (Array.isArray(n?.children) ? n.children : []);
8
+
9
+ const walk = (node, parent, fn) => {
10
+ if (!node || typeof node !== 'object') return;
11
+ fn && fn(node, parent);
12
+ const kids = getChildren(node);
13
+ for (const child of kids) walk(child, node, fn);
14
+ };
15
+
16
+ const ensureArray = (v) => (Array.isArray(v) ? v : v != null ? [v] : []);
17
+ const hasClass = (el, name) => ensureArray(el?.properties?.className).map(String).includes(name);
18
+
19
+ const wrapTable = (tableNode, parent) => {
20
+ if (!parent || !Array.isArray(parent.children)) return;
21
+ // Don't double-wrap if already inside .table-scroll
22
+ if (parent.tagName === 'div' && hasClass(parent, 'table-scroll')) return;
23
+
24
+ const wrapper = {
25
+ type: 'element',
26
+ tagName: 'div',
27
+ properties: { className: ['table-scroll'] },
28
+ children: [tableNode]
29
+ };
30
+
31
+ const idx = parent.children.indexOf(tableNode);
32
+ if (idx >= 0) parent.children.splice(idx, 1, wrapper);
33
+ };
34
+
35
+ walk(tree, null, (node, parent) => {
36
+ if (!isElement(node)) return;
37
+ if (node.tagName !== 'table') return;
38
+ wrapTable(node, parent);
39
+ });
40
+ };
41
+ }
42
+
43
+
app/plugins/remark/ignore-citations-in-code.mjs ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Remark plugin to ignore citations inside code (block and inline)
2
+ export default function remarkIgnoreCitationsInCode() {
3
+ return (tree) => {
4
+ const visit = (node) => {
5
+ if (!node || typeof node !== 'object') return;
6
+ const type = node.type;
7
+ if (type === 'code' || type === 'inlineCode') {
8
+ if (typeof node.value === 'string' && node.value.includes('@')) {
9
+ // Use a sentinel to avoid rehype-citation, will be restored later in rehype
10
+ node.value = node.value.replace(/@/g, '__AT_SENTINEL__');
11
+ }
12
+ return; // do not traverse into code
13
+ }
14
+ const children = Array.isArray(node.children) ? node.children : [];
15
+ children.forEach(visit);
16
+ };
17
+ visit(tree);
18
+ };
19
+ }
20
+
21
+
app/plugins/remark/output-container.mjs ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Transform `:::output ... :::` into a <section class="code-output"> wrapper
2
+ // Requires remark-directive to be applied before this plugin
3
+
4
+ export default function remarkOutputContainer() {
5
+ return (tree) => {
6
+ const visit = (node) => {
7
+ if (!node || typeof node !== 'object') return;
8
+
9
+ if (node.type === 'containerDirective' && node.name === 'output') {
10
+ node.data = node.data || {};
11
+ node.data.hName = 'section';
12
+ node.data.hProperties = { className: ['code-output'] };
13
+ }
14
+
15
+ const children = Array.isArray(node.children) ? node.children : [];
16
+ for (const child of children) visit(child);
17
+ };
18
+
19
+ visit(tree);
20
+ };
21
+ }
22
+
23
+
app/plugins/remark/outputs-container.mjs ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Transform `:::outputs ... :::` into a <section class="code-outputs"> wrapper
2
+ // Requires remark-directive to be applied before this plugin
3
+
4
+ export default function remarkOutputsContainer() {
5
+ return (tree) => {
6
+ const visit = (node) => {
7
+ if (!node || typeof node !== 'object') return;
8
+
9
+ if (node.type === 'containerDirective' && node.name === 'outputs') {
10
+ node.data = node.data || {};
11
+ node.data.hName = 'section';
12
+ node.data.hProperties = { className: ['code-outputs'] };
13
+ }
14
+
15
+ const children = Array.isArray(node.children) ? node.children : [];
16
+ for (const child of children) visit(child);
17
+ };
18
+
19
+ visit(tree);
20
+ };
21
+ }
22
+
23
+
app/plugins/remark/unwrap-citation-links.mjs ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Plugin remark pour transformer les liens markdown contenant des citations en citations simples
2
+ // Transforme [@reference](url) en [@reference]
3
+ export default function remarkUnwrapCitationLinks() {
4
+ return (tree) => {
5
+ // Fonction helper pour extraire le contenu textuel d'un nΕ“ud
6
+ const getTextContent = (node) => {
7
+ if (!node) return '';
8
+ if (node.type === 'text') return node.value || '';
9
+ if (Array.isArray(node.children)) {
10
+ return node.children.map(getTextContent).join('');
11
+ }
12
+ return '';
13
+ };
14
+
15
+ const visit = (node, parent) => {
16
+ if (!node || typeof node !== 'object') return;
17
+
18
+ // Parcourir les enfants d'abord (post-order traversal)
19
+ const children = Array.isArray(node.children) ? node.children : [];
20
+ for (let i = 0; i < children.length; i++) {
21
+ const child = children[i];
22
+ visit(child, node);
23
+ }
24
+
25
+ // Si c'est un nΕ“ud de type 'link', vΓ©rifier son contenu
26
+ if (node.type === 'link' && parent && Array.isArray(parent.children)) {
27
+ // RΓ©cupΓ©rer le contenu textuel du lien
28
+ const textContent = getTextContent(node);
29
+
30
+ // Debug
31
+ console.log('πŸ” Link trouvΓ©:', {
32
+ text: textContent,
33
+ url: node.url,
34
+ matches: /^@\w+/.test(textContent.trim())
35
+ });
36
+
37
+ // VΓ©rifier si c'est une citation (commence par @)
38
+ if (textContent && /^@\w+/.test(textContent.trim())) {
39
+ // Trouver l'index du nΕ“ud dans son parent
40
+ const index = parent.children.indexOf(node);
41
+
42
+ if (index !== -1) {
43
+ console.log('βœ… Transformation:', textContent);
44
+ // Remplacer le nΕ“ud link par un nΕ“ud text simple
45
+ parent.children[index] = {
46
+ type: 'text',
47
+ value: textContent.trim()
48
+ };
49
+ }
50
+ }
51
+ }
52
+ };
53
+
54
+ visit(tree, null);
55
+ };
56
+ }
57
+
app/postcss.config.mjs ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // PostCSS config enabling Custom Media Queries
2
+ // Allows usage of: @media (--bp-content-collapse) { ... }
3
+
4
+ import postcssCustomMedia from 'postcss-custom-media';
5
+ import postcssPresetEnv from 'postcss-preset-env';
6
+
7
+ export default {
8
+ plugins: [
9
+ postcssCustomMedia(),
10
+ postcssPresetEnv({
11
+ stage: 0
12
+ })
13
+ ]
14
+ };
app/public/flowchart-svg.svg ADDED
app/public/hf-space-parent-listener.js ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Script pour la fenΓͺtre parente des Spaces Hugging Face
3
+ * Ce script Γ©coute les messages de l'iframe et met Γ  jour l'URL de la fenΓͺtre parente
4
+ *
5
+ * Instructions d'utilisation :
6
+ * 1. Ajoutez ce script Γ  votre Space Hugging Face dans le fichier app.py ou dans un composant Gradio
7
+ * 2. Ou utilisez-le dans une page HTML qui contient votre iframe
8
+ */
9
+
10
+ (function () {
11
+ 'use strict';
12
+
13
+ console.log('HF Space Parent Listener initialized');
14
+
15
+ // Γ‰couter les messages de l'iframe
16
+ window.addEventListener('message', function (event) {
17
+ console.log('Received message from iframe:', event.data);
18
+
19
+ // VΓ©rifier le type de message
20
+ if (event.data && event.data.type) {
21
+ switch (event.data.type) {
22
+ case 'urlChange':
23
+ case 'anchorChange':
24
+ case 'HF_SPACE_URL_UPDATE':
25
+ handleUrlChange(event.data);
26
+ break;
27
+ default:
28
+ console.log('Unknown message type:', event.data.type);
29
+ }
30
+ }
31
+ });
32
+
33
+ function handleUrlChange(data) {
34
+ try {
35
+ const hash = data.hash || data.anchorId;
36
+ const url = data.url;
37
+
38
+ if (hash) {
39
+ // Mettre Γ  jour l'URL avec la nouvelle ancre
40
+ const newUrl = new URL(window.location);
41
+ newUrl.hash = hash;
42
+
43
+ // Utiliser replaceState pour Γ©viter d'ajouter une entrΓ©e dans l'historique
44
+ window.history.replaceState(null, '', newUrl.toString());
45
+
46
+ console.log('Updated parent URL to:', newUrl.toString());
47
+
48
+ // Optionnel : faire dΓ©filer vers l'Γ©lΓ©ment correspondant dans la page parente
49
+ const targetElement = document.querySelector(hash);
50
+ if (targetElement) {
51
+ targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
52
+ }
53
+ }
54
+ } catch (error) {
55
+ console.error('Error updating parent URL:', error);
56
+ }
57
+ }
58
+
59
+ // Fonction utilitaire pour tester la communication
60
+ window.testIframeCommunication = function () {
61
+ console.log('Testing iframe communication...');
62
+ const iframe = document.querySelector('iframe');
63
+ if (iframe) {
64
+ iframe.contentWindow.postMessage({ type: 'test' }, '*');
65
+ } else {
66
+ console.log('No iframe found');
67
+ }
68
+ };
69
+
70
+ })();
app/public/og.png ADDED

Git LFS Details

  • SHA256: dacac0b7026402fd2c00cbb878474f6399db1d7c5a89e079148ba97558432143
  • Pointer size: 131 Bytes
  • Size of remote file: 137 kB
app/public/scripts/color-palettes.js ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Global color palettes generator and watcher
2
+ // - Observes CSS variable --primary-color and theme changes
3
+ // - Generates categorical, sequential, and diverging palettes (OKLCH/OKLab)
4
+ // - Exposes results as CSS variables on :root
5
+ // - Supports variable color counts per palette via CSS vars
6
+ // - Dispatches a 'palettes:updated' CustomEvent after each update
7
+
8
+ (() => {
9
+ const MODE = { cssRoot: document.documentElement };
10
+
11
+ const getCssVar = (name) => {
12
+ try { return getComputedStyle(MODE.cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
13
+ };
14
+ const getIntFromCssVar = (name, fallback) => {
15
+ const raw = getCssVar(name);
16
+ if (!raw) return fallback;
17
+ const v = parseInt(String(raw), 10);
18
+ if (Number.isNaN(v)) return fallback;
19
+ return v;
20
+ };
21
+ const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
22
+
23
+ // Color math (OKLab/OKLCH)
24
+ const srgbToLinear = (u) => (u <= 0.04045 ? u / 12.92 : Math.pow((u + 0.055) / 1.055, 2.4));
25
+ const linearToSrgb = (u) => (u <= 0.0031308 ? 12.92 * u : 1.055 * Math.pow(Math.max(0, u), 1 / 2.4) - 0.055);
26
+ const rgbToOklab = (r, g, b) => {
27
+ const rl = srgbToLinear(r), gl = srgbToLinear(g), bl = srgbToLinear(b);
28
+ const l = Math.cbrt(0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl);
29
+ const m = Math.cbrt(0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl);
30
+ const s = Math.cbrt(0.0883024619 * rl + 0.2817188376 * gl + 0.6299787005 * bl);
31
+ const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
32
+ const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
33
+ const b2 = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
34
+ return { L, a, b: b2 };
35
+ };
36
+ const oklabToRgb = (L, a, b) => {
37
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
38
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
39
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
40
+ const l = l_ * l_ * l_;
41
+ const m = m_ * m_ * m_;
42
+ const s = s_ * s_ * s_;
43
+ const r = linearToSrgb(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s);
44
+ const g = linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s);
45
+ const b3 = linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s);
46
+ return { r, g, b: b3 };
47
+ };
48
+ const oklchToOklab = (L, C, hDeg) => { const h = (hDeg * Math.PI) / 180; return { L, a: C * Math.cos(h), b: C * Math.sin(h) }; };
49
+ const oklabToOklch = (L, a, b) => { const C = Math.sqrt(a * a + b * b); let h = Math.atan2(b, a) * 180 / Math.PI; if (h < 0) h += 360; return { L, C, h }; };
50
+ const clamp01 = (x) => Math.min(1, Math.max(0, x));
51
+ const isInGamut = ({ r, g, b }) => r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;
52
+ const toHex = ({ r, g, b }) => {
53
+ const R = Math.round(clamp01(r) * 255), G = Math.round(clamp01(g) * 255), B = Math.round(clamp01(b) * 255);
54
+ const h = (n) => n.toString(16).padStart(2, '0');
55
+ return `#${h(R)}${h(G)}${h(B)}`.toUpperCase();
56
+ };
57
+ const oklchToHexSafe = (L, C, h) => { let c = C; for (let i = 0; i < 12; i++) { const { a, b } = oklchToOklab(L, c, h); const rgb = oklabToRgb(L, a, b); if (isInGamut(rgb)) return toHex(rgb); c = Math.max(0, c - 0.02); } return toHex(oklabToRgb(L, 0, 0)); };
58
+ const parseCssColorToRgb = (css) => { try { const el = document.createElement('span'); el.style.color = css; document.body.appendChild(el); const cs = getComputedStyle(el).color; document.body.removeChild(el); const m = cs.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!m) return null; return { r: Number(m[1]) / 255, g: Number(m[2]) / 255, b: Number(m[3]) / 255 }; } catch { return null; } };
59
+
60
+ // Get primary color in OKLCH format to preserve precision
61
+ const getPrimaryOKLCH = () => {
62
+ const css = getCssVar('--primary-color');
63
+ if (!css) return null;
64
+
65
+ // For OKLCH colors, return the exact values without conversion
66
+ if (css.includes('oklch')) {
67
+ const oklchMatch = css.match(/oklch\(([^)]+)\)/);
68
+ if (oklchMatch) {
69
+ const values = oklchMatch[1].split(/\s+/).map(v => parseFloat(v.trim()));
70
+ if (values.length >= 3) {
71
+ const [L, C, h] = values;
72
+ return { L, C, h };
73
+ }
74
+ }
75
+ }
76
+
77
+ // For non-OKLCH colors, convert to OKLCH for consistency
78
+ const rgb = parseCssColorToRgb(css);
79
+ if (rgb) {
80
+ const { L, a, b } = rgbToOklab(rgb.r, rgb.g, rgb.b);
81
+ const { C, h } = oklabToOklch(L, a, b);
82
+ return { L, C, h };
83
+ }
84
+ return null;
85
+ };
86
+
87
+ // Keep getPrimaryHex for backward compatibility, but now it converts from OKLCH
88
+ const getPrimaryHex = () => {
89
+ const oklch = getPrimaryOKLCH();
90
+ if (!oklch) return null;
91
+
92
+ const { a, b } = oklchToOklab(oklch.L, oklch.C, oklch.h);
93
+ const rgb = oklabToRgb(oklch.L, a, b);
94
+ return toHex(rgb);
95
+ };
96
+ // No count management via CSS anymore; counts are passed directly to the API
97
+
98
+ const generators = {
99
+ categorical: (baseOKLCH, count) => {
100
+ const { L, C, h } = baseOKLCH;
101
+ const L0 = Math.min(0.85, Math.max(0.4, L));
102
+ const C0 = Math.min(0.35, Math.max(0.1, C || 0.2));
103
+ const total = Math.max(1, Math.min(12, count || 8));
104
+ const hueStep = 360 / total;
105
+ const results = [];
106
+ for (let i = 0; i < total; i++) {
107
+ const hDeg = (h + i * hueStep) % 360;
108
+ const lVar = ((i % 3) - 1) * 0.04;
109
+ results.push(oklchToHexSafe(Math.max(0.4, Math.min(0.85, L0 + lVar)), C0, hDeg));
110
+ }
111
+ return results;
112
+ },
113
+ sequential: (baseOKLCH, count) => {
114
+ const { L, C, h } = baseOKLCH;
115
+ const total = Math.max(1, Math.min(12, count || 8));
116
+ const startL = Math.max(0.25, L - 0.18);
117
+ const endL = Math.min(0.92, L + 0.18);
118
+ const cBase = Math.min(0.33, Math.max(0.08, C * 0.9 + 0.06));
119
+ const out = [];
120
+ for (let i = 0; i < total; i++) {
121
+ const t = total === 1 ? 0 : i / (total - 1);
122
+ const lNow = startL * (1 - t) + endL * t;
123
+ const cNow = cBase * (0.85 + 0.15 * (1 - Math.abs(0.5 - t) * 2));
124
+ out.push(oklchToHexSafe(lNow, cNow, h));
125
+ }
126
+ return out;
127
+ },
128
+ diverging: (baseOKLCH, count) => {
129
+ const { L, C, h } = baseOKLCH;
130
+ const total = Math.max(1, Math.min(12, count || 8));
131
+
132
+ // Left endpoint: EXACT primary color (no darkening)
133
+ const leftLab = oklchToOklab(L, C, h);
134
+ // Right endpoint: complement with same L and similar C (clamped safe)
135
+ const compH = (h + 180) % 360;
136
+ const cSafe = Math.min(0.35, Math.max(0.08, C));
137
+ const rightLab = oklchToOklab(L, cSafe, compH);
138
+ const whiteLab = { L: 0.98, a: 0, b: 0 }; // center near‑white
139
+
140
+ const hexFromOKLab = (L, a, b) => toHex(oklabToRgb(L, a, b));
141
+ const lerp = (a, b, t) => a + (b - a) * t;
142
+ const lerpOKLabHex = (A, B, t) => hexFromOKLab(lerp(A.L, B.L, t), lerp(A.a, B.a, t), lerp(A.b, B.b, t));
143
+
144
+ const out = [];
145
+ if (total % 2 === 1) {
146
+ const nSide = (total - 1) >> 1; // items on each side
147
+ // Left side: include left endpoint exactly at index 0
148
+ for (let i = 0; i < nSide; i++) {
149
+ const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
150
+ // Move from leftLab to a value close (but not equal) to white; ensure last before center is lighter
151
+ const tt = t * 0.9; // keep some distance from pure white before center
152
+ out.push(lerpOKLabHex(leftLab, whiteLab, tt));
153
+ }
154
+ // Center
155
+ out.push(hexFromOKLab(whiteLab.L, whiteLab.a, whiteLab.b));
156
+ // Right side: start near white and end EXACTLY at rightLab
157
+ for (let i = 0; i < nSide; i++) {
158
+ const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
159
+ const tt = Math.max(0.1, t); // avoid starting at pure white
160
+ out.push(lerpOKLabHex(whiteLab, rightLab, tt));
161
+ }
162
+ // Ensure first and last are exact endpoints
163
+ if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
164
+ } else {
165
+ const nSide = total >> 1;
166
+ // Left half including left endpoint, approaching white but not reaching it
167
+ for (let i = 0; i < nSide; i++) {
168
+ const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
169
+ const tt = t * 0.9;
170
+ out.push(lerpOKLabHex(leftLab, whiteLab, tt));
171
+ }
172
+ // Right half: mirror from near white to exact right endpoint
173
+ for (let i = 0; i < nSide; i++) {
174
+ const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
175
+ const tt = Math.max(0.1, t);
176
+ out.push(lerpOKLabHex(whiteLab, rightLab, tt));
177
+ }
178
+ if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
179
+ }
180
+ return out;
181
+ }
182
+ };
183
+
184
+ let lastSignature = '';
185
+
186
+ const updatePalettes = () => {
187
+ const primaryOKLCH = getPrimaryOKLCH();
188
+ const primaryHex = getPrimaryHex();
189
+ const signature = `${primaryOKLCH?.L},${primaryOKLCH?.C},${primaryOKLCH?.h}`;
190
+ if (signature === lastSignature) return;
191
+ lastSignature = signature;
192
+ try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { }
193
+ };
194
+
195
+ const bootstrap = () => {
196
+ // Initial setup - only run once on page load
197
+ updatePalettes();
198
+
199
+ // Observer will handle all subsequent changes
200
+ const mo = new MutationObserver(() => updatePalettes());
201
+ mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
202
+
203
+ // Utility: choose high-contrast (or softened) text style against an arbitrary background color
204
+ const pickTextStyleForBackground = (bgCss, opts = {}) => {
205
+ const cssRoot = document.documentElement;
206
+ const getCssVar = (name) => {
207
+ try { return getComputedStyle(cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
208
+ };
209
+ const resolveCssToRgb01 = (css) => {
210
+ const rgb = parseCssColorToRgb(css);
211
+ if (!rgb) return null;
212
+ return rgb; // already 0..1
213
+ };
214
+ const mixRgb01 = (a, b, t) => ({ r: a.r * (1 - t) + b.r * t, g: a.g * (1 - t) + b.g * t, b: a.b * (1 - t) + b.b * t });
215
+ const relLum = (rgb) => {
216
+ const f = (u) => srgbToLinear(u);
217
+ return 0.2126 * f(rgb.r) + 0.7152 * f(rgb.g) + 0.0722 * f(rgb.b);
218
+ };
219
+ const contrast = (fg, bg) => {
220
+ const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1, L2), b = Math.min(L1, L2);
221
+ return (a + 0.05) / (b + 0.05);
222
+ };
223
+ try {
224
+ const bg = resolveCssToRgb01(bgCss);
225
+ if (!bg) return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
226
+ const candidatesCss = [getCssVar('--text-color') || '#111', getCssVar('--on-primary') || '#0f1115', '#000', '#fff'];
227
+ const candidates = candidatesCss
228
+ .map(css => ({ css, rgb: resolveCssToRgb01(css) }))
229
+ .filter(x => !!x.rgb);
230
+ // Pick the max contrast
231
+ let best = candidates[0]; let bestCR = contrast(best.rgb, bg);
232
+ for (let i = 1; i < candidates.length; i++) {
233
+ const cr = contrast(candidates[i].rgb, bg);
234
+ if (cr > bestCR) { best = candidates[i]; bestCR = cr; }
235
+ }
236
+ // Optional softening via blend factor (0..1), blending towards muted color
237
+ const blend = Math.min(1, Math.max(0, Number(opts.blend || 0)));
238
+ let finalRgb = best.rgb;
239
+ if (blend > 0) {
240
+ const mutedCss = getCssVar('--muted-color') || (getCssVar('--text-color') || '#111');
241
+ const mutedRgb = resolveCssToRgb01(mutedCss) || best.rgb;
242
+ finalRgb = mixRgb01(best.rgb, mutedRgb, blend);
243
+ }
244
+ const haloStrength = Math.min(1, Math.max(0, Number(opts.haloStrength == null ? 0.5 : opts.haloStrength)));
245
+ const stroke = (best.css === '#000' || best.css.toLowerCase() === 'black') ? `rgba(255,255,255,${0.30 + 0.40 * haloStrength})` : `rgba(0,0,0,${0.30 + 0.30 * haloStrength})`;
246
+ return { fill: toHex(finalRgb), stroke, strokeWidth: (opts.haloWidth == null ? 1 : Number(opts.haloWidth)) };
247
+ } catch {
248
+ return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
249
+ }
250
+ };
251
+ window.ColorPalettes = {
252
+ refresh: updatePalettes,
253
+ notify: () => { try { const primaryOKLCH = getPrimaryOKLCH(); const primaryHex = getPrimaryHex(); document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { } },
254
+ getPrimary: () => getPrimaryHex(),
255
+ getPrimaryOKLCH: () => getPrimaryOKLCH(),
256
+ getColors: (key, count = 6) => {
257
+ const primaryOKLCH = getPrimaryOKLCH();
258
+ if (!primaryOKLCH) return [];
259
+ const total = Math.max(1, Math.min(12, Number(count) || 6));
260
+ if (key === 'categorical') return generators.categorical(primaryOKLCH, total);
261
+ if (key === 'sequential') return generators.sequential(primaryOKLCH, total);
262
+ if (key === 'diverging') return generators.diverging(primaryOKLCH, total);
263
+ return [];
264
+ },
265
+ getTextStyleForBackground: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}),
266
+ chooseReadableText: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {})
267
+ };
268
+ };
269
+
270
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
271
+ else bootstrap();
272
+ })();
273
+
274
+
app/src/components/Footer.astro ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ interface Props {
3
+ citationText: string;
4
+ bibtex: string;
5
+ licence?: string;
6
+ doi?: string;
7
+ }
8
+ const { citationText, bibtex, licence, doi } = Astro.props as Props;
9
+ ---
10
+
11
+ <footer class="footer">
12
+ <div class="footer-inner">
13
+ <section class="citation-block">
14
+ <h3>Citation</h3>
15
+ <p>For attribution in academic contexts, please cite this work as</p>
16
+ <pre class="citation short">{citationText}</pre>
17
+
18
+ <p>BibTeX citation</p>
19
+ <pre class="citation long">{bibtex}</pre>
20
+ </section>
21
+ {
22
+ doi && (
23
+ <section class="doi-block">
24
+ <h3>DOI</h3>
25
+ <p>
26
+ <a
27
+ href={`https://doi.org/${doi}`}
28
+ target="_blank"
29
+ rel="noopener noreferrer"
30
+ >
31
+ {doi}
32
+ </a>
33
+ </p>
34
+ </section>
35
+ )
36
+ }
37
+ {
38
+ licence && (
39
+ <section class="reuse-block">
40
+ <h3>Reuse</h3>
41
+ <p set:html={licence} />
42
+ </section>
43
+ )
44
+ }
45
+ <section class="references-block">
46
+ <slot />
47
+ </section>
48
+ <div class="template-credit">
49
+ <p>
50
+ Made with ❀️ with <a
51
+ href="https://huggingface.co/spaces/tfrere/research-article-template"
52
+ target="_blank"
53
+ rel="noopener noreferrer">research article template</a
54
+ >
55
+ </p>
56
+ </div>
57
+ </div>
58
+ </footer>
59
+
60
+ <script is:inline>
61
+ (() => {
62
+ const getFooter = () =>
63
+ document.currentScript?.closest("footer") ||
64
+ document.querySelector("footer.footer");
65
+ const footer = getFooter();
66
+ if (!footer) return;
67
+ const target = footer.querySelector(".references-block");
68
+ if (!target) return;
69
+
70
+ const contentRoot =
71
+ document.querySelector("section.content-grid main") ||
72
+ document.querySelector("main") ||
73
+ document.body;
74
+
75
+ const ensureHeading = (text) => {
76
+ const exists = Array.from(target.children).some(
77
+ (c) =>
78
+ c.tagName === "H3" &&
79
+ c.textContent.trim().toLowerCase() === text.toLowerCase(),
80
+ );
81
+ if (!exists) {
82
+ const h = document.createElement("h3");
83
+ h.textContent = text;
84
+ target.appendChild(h);
85
+ }
86
+ };
87
+
88
+ const moveIntoFooter = (element, headingText) => {
89
+ if (!element) return false;
90
+ // Remove an eventual heading already included inside the block (avoid duplicates)
91
+ const firstHeading = element.querySelector(
92
+ ":scope > h1, :scope > h2, :scope > h3",
93
+ );
94
+ if (firstHeading) {
95
+ const txt = (firstHeading.textContent || "").trim().toLowerCase();
96
+ const targetTxt = headingText.trim().toLowerCase();
97
+ if (
98
+ txt === targetTxt ||
99
+ txt.includes("reference") ||
100
+ txt.includes("bibliograph")
101
+ ) {
102
+ firstHeading.remove();
103
+ }
104
+ }
105
+
106
+ // Move footnote backref links inside paragraphs
107
+ if (element.classList && element.classList.contains("footnotes")) {
108
+ const footnoteItems = element.querySelectorAll("li");
109
+ footnoteItems.forEach((item) => {
110
+ const backrefContainer = item.querySelector("small.backrefs");
111
+ const lastP = item.querySelector("p:last-of-type");
112
+ if (backrefContainer && lastP && !lastP.contains(backrefContainer)) {
113
+ lastP.appendChild(document.createTextNode(" "));
114
+ lastP.appendChild(backrefContainer);
115
+ }
116
+ });
117
+ }
118
+
119
+ ensureHeading(headingText);
120
+ target.appendChild(element);
121
+ return true;
122
+ };
123
+ const run = () => {
124
+ const findFirstOutsideFooter = (selectors) => {
125
+ for (const sel of selectors) {
126
+ const el = contentRoot.querySelector(sel);
127
+ if (el && !footer.contains(el)) return el;
128
+ }
129
+ return null;
130
+ };
131
+
132
+ const referencesEl = findFirstOutsideFooter([
133
+ "#bibliography-references-list",
134
+ "[data-bibliography-block]",
135
+ "#references",
136
+ "#refs",
137
+ ".references",
138
+ ".bibliography",
139
+ ]);
140
+ const footnotesEl = findFirstOutsideFooter([".footnotes"]);
141
+
142
+ const movedRefs = moveIntoFooter(referencesEl, "References");
143
+ const movedNotes = moveIntoFooter(footnotesEl, "Footnotes");
144
+ return movedRefs || movedNotes;
145
+ };
146
+
147
+ // Try now; if not found yet, try again on DOM ready
148
+ const done = run();
149
+ if (!done) {
150
+ const onReady = () => run();
151
+ if (document.readyState === "loading") {
152
+ document.addEventListener("DOMContentLoaded", onReady, { once: true });
153
+ } else {
154
+ setTimeout(onReady, 0);
155
+ }
156
+ }
157
+
158
+ // Resize on window changes (e.g., fonts, layout)
159
+ // No textarea auto-resize needed for <pre> blocks
160
+ })();
161
+ </script>
162
+
163
+ <style is:global>
164
+ .footer {
165
+ contain: layout style;
166
+ font-size: 0.8em;
167
+ line-height: 1.7em;
168
+ margin-top: 60px;
169
+ margin-bottom: 0;
170
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
171
+ color: rgba(0, 0, 0, 0.5);
172
+ }
173
+
174
+ .footer-inner {
175
+ max-width: 1280px;
176
+ margin: 0 auto;
177
+ padding: 60px 16px 48px;
178
+ display: grid;
179
+ grid-template-columns: 220px minmax(0, 680px) 260px;
180
+ /* Every citation child is its own grid row, so a symmetric gap spaces the
181
+ block out far more than the columns need. */
182
+ column-gap: 32px;
183
+ row-gap: 12px;
184
+ align-items: start;
185
+ }
186
+
187
+ /* Use the parent grid (3 columns like .content-grid) */
188
+ .citation-block,
189
+ .references-block,
190
+ .reuse-block,
191
+ .doi-block {
192
+ display: contents;
193
+ }
194
+
195
+ .citation-block > h3,
196
+ .references-block > h3,
197
+ .reuse-block > h3,
198
+ .doi-block > h3 {
199
+ grid-column: 1;
200
+ font-size: 15px;
201
+ margin: 0;
202
+ text-align: right;
203
+ padding-right: 30px;
204
+ }
205
+
206
+ .citation-block > :not(h3),
207
+ .references-block > :not(h3),
208
+ .reuse-block > :not(h3),
209
+ .doi-block > :not(h3) {
210
+ grid-column: 2;
211
+ }
212
+
213
+ .citation-block h3 {
214
+ margin: 0 0 8px;
215
+ }
216
+
217
+ .citation-block h4 {
218
+ margin: 16px 0 8px;
219
+ font-size: 14px;
220
+ text-transform: uppercase;
221
+ color: var(--muted-color);
222
+ }
223
+
224
+ .citation-block p,
225
+ .reuse-block p,
226
+ .doi-block p,
227
+ .footnotes ol,
228
+ .footnotes ol p,
229
+ .references {
230
+ margin-top: 0;
231
+ }
232
+
233
+ /* Distill-like appendix citation styling */
234
+ .citation {
235
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
236
+ "Liberation Mono", "Courier New", monospace;
237
+ font-size: 11px;
238
+ line-height: 15px;
239
+ border-left: 1px solid rgba(0, 0, 0, 0.1);
240
+ padding-left: 18px;
241
+ border: 1px solid rgba(0, 0, 0, 0.1);
242
+ background: rgba(0, 0, 0, 0.02);
243
+ padding: 10px 18px;
244
+ border-radius: 3px;
245
+ color: rgba(150, 150, 150, 1);
246
+ overflow: hidden;
247
+ margin-top: -12px;
248
+ white-space: pre-wrap;
249
+ word-wrap: break-word;
250
+ }
251
+
252
+ .citation a {
253
+ color: rgba(0, 0, 0, 0.6);
254
+ text-decoration: underline;
255
+ }
256
+
257
+ .citation.short {
258
+ margin-top: -4px;
259
+ }
260
+
261
+ .references-block h3 {
262
+ margin: 0;
263
+ }
264
+
265
+ /* Distill-like list styling for references/footnotes */
266
+ .references-block ol {
267
+ padding: 0 0 0 15px;
268
+ }
269
+
270
+ @media (min-width: 768px) {
271
+ .references-block ol {
272
+ padding: 0 0 0 30px;
273
+ margin-left: -30px;
274
+ }
275
+ }
276
+
277
+ .references-block li {
278
+ margin-bottom: 1em;
279
+ }
280
+
281
+ .references-block a {
282
+ color: var(--text-color);
283
+ }
284
+
285
+ [data-theme="dark"] .footer {
286
+ border-top-color: rgba(255, 255, 255, 0.15);
287
+ color: rgba(200, 200, 200, 0.8);
288
+ }
289
+ [data-theme="dark"] .citation {
290
+ background: rgba(255, 255, 255, 0.04);
291
+ border-color: rgba(255, 255, 255, 0.15);
292
+ color: rgba(200, 200, 200, 1);
293
+ }
294
+ [data-theme="dark"] .citation a {
295
+ color: rgba(255, 255, 255, 0.75);
296
+ }
297
+
298
+ /* Footer links: use primary color consistently */
299
+ .footer a {
300
+ color: var(--primary-color);
301
+ border-bottom: 1px solid var(--link-underline);
302
+ text-decoration: none;
303
+ }
304
+ .footer a:hover {
305
+ color: var(--primary-color-hover);
306
+ border-bottom-color: var(--link-underline-hover);
307
+ }
308
+ [data-theme="dark"] .footer a {
309
+ color: var(--primary-color);
310
+ }
311
+
312
+ /* Template credit - discrete at the bottom */
313
+ .template-credit {
314
+ display: contents;
315
+ }
316
+
317
+ .template-credit p {
318
+ grid-column: 2;
319
+ margin: 24px 0 0 0;
320
+ font-size: 0.85em;
321
+ color: rgba(0, 0, 0, 0.5);
322
+ }
323
+
324
+ .template-credit a {
325
+ color: rgba(0, 0, 0, 0.6);
326
+ border-bottom: 1px solid rgba(0, 0, 0, 0.15);
327
+ }
328
+
329
+ .template-credit a:hover {
330
+ color: rgba(0, 0, 0, 0.8);
331
+ border-bottom-color: rgba(0, 0, 0, 0.3);
332
+ }
333
+
334
+ [data-theme="dark"] .template-credit p {
335
+ color: rgba(200, 200, 200, 0.6);
336
+ }
337
+
338
+ [data-theme="dark"] .template-credit a {
339
+ color: rgba(200, 200, 200, 0.7);
340
+ border-bottom-color: rgba(255, 255, 255, 0.2);
341
+ }
342
+
343
+ [data-theme="dark"] .template-credit a:hover {
344
+ color: rgba(200, 200, 200, 0.9);
345
+ border-bottom-color: rgba(255, 255, 255, 0.35);
346
+ }
347
+ </style>
app/src/components/FullWidth.astro ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ const { class: className, ...props } = Astro.props;
3
+ const wrapperClass = ["full-width", className].filter(Boolean).join(" ");
4
+ ---
5
+ <div class={wrapperClass} {...props}>
6
+ <slot />
7
+ </div>
8
+
9
+
app/src/components/Hero.astro ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ import HtmlEmbed from "./HtmlEmbed.astro";
3
+
4
+ interface Props {
5
+ title: string; // may contain HTML (e.g., <br/>)
6
+ titleRaw?: string; // plain title for slug/PDF (optional)
7
+ description?: string;
8
+ authors?: Array<
9
+ string | { name: string; url?: string; affiliationIndices?: number[] }
10
+ >;
11
+ affiliations?: Array<{ id: number; name: string; url?: string }>;
12
+ affiliation?: string; // legacy single affiliation
13
+ published?: string;
14
+ doi?: string;
15
+ arxiv?: string;
16
+ pdfProOnly?: boolean; // Gate PDF download to Pro users only
17
+ }
18
+
19
+ const {
20
+ title,
21
+ titleRaw,
22
+ description,
23
+ authors = [],
24
+ affiliations = [],
25
+ affiliation,
26
+ published,
27
+ doi,
28
+ arxiv,
29
+ pdfProOnly = false,
30
+ } = Astro.props as Props;
31
+
32
+ type Author = { name: string; url?: string; affiliationIndices?: number[] };
33
+
34
+ function normalizeAuthors(
35
+ input: Array<
36
+ | string
37
+ | {
38
+ name?: string;
39
+ url?: string;
40
+ link?: string;
41
+ affiliationIndices?: number[];
42
+ }
43
+ >,
44
+ ): Author[] {
45
+ return (Array.isArray(input) ? input : [])
46
+ .map((a) => {
47
+ if (typeof a === "string") {
48
+ return { name: a } as Author;
49
+ }
50
+ const name = (a?.name ?? "").toString();
51
+ const url = (a?.url ?? a?.link) as string | undefined;
52
+ const affiliationIndices = Array.isArray((a as any)?.affiliationIndices)
53
+ ? (a as any).affiliationIndices
54
+ : undefined;
55
+ return { name, url, affiliationIndices } as Author;
56
+ })
57
+ .filter((a) => a.name && a.name.trim().length > 0);
58
+ }
59
+
60
+ const normalizedAuthors: Author[] = normalizeAuthors(authors as any);
61
+
62
+ // Determine if affiliation superscripts should be shown (only when there are multiple distinct affiliations referenced by authors)
63
+ const authorAffiliationIndexSet = new Set<number>();
64
+ for (const author of normalizedAuthors) {
65
+ const indices = Array.isArray(author.affiliationIndices)
66
+ ? author.affiliationIndices
67
+ : [];
68
+ for (const idx of indices) {
69
+ if (typeof idx === "number") {
70
+ authorAffiliationIndexSet.add(idx);
71
+ }
72
+ }
73
+ }
74
+ const shouldShowAffiliationSupers = authorAffiliationIndexSet.size > 1;
75
+ const hasMultipleAffiliations =
76
+ Array.isArray(affiliations) && affiliations.length > 1;
77
+
78
+ function stripHtml(text: string): string {
79
+ return String(text || "").replace(/<[^>]*>/g, "");
80
+ }
81
+
82
+ function slugify(text: string): string {
83
+ return (
84
+ String(text || "")
85
+ .normalize("NFKD")
86
+ .replace(/\p{Diacritic}+/gu, "")
87
+ .toLowerCase()
88
+ .replace(/[^a-z0-9]+/g, "-")
89
+ .replace(/^-+|-+$/g, "")
90
+ .slice(0, 120) || "article"
91
+ );
92
+ }
93
+
94
+ const pdfBase = titleRaw ? stripHtml(titleRaw) : stripHtml(title);
95
+ const pdfFilename = `${slugify(pdfBase)}.pdf`;
96
+ ---
97
+
98
+ <section class="hero">
99
+ <h1 class="hero-title" set:html={title} />
100
+ <div class="hero-banner">
101
+ <HtmlEmbed src="branch-explorer.html" frameless />
102
+ {description && <p class="hero-desc">{description}</p>}
103
+ </div>
104
+ <div class="hero-features" id="hero-features">
105
+ <a class="hero-feat-badge" href="https://neuronpedia.org/gemma-2-2b/20-gemmascope-res-16k/14304" target="_blank" rel="noopener">
106
+ <span class="feat-id">L20/14304</span>
107
+ <span class="feat-desc">merging components in a process</span>
108
+ </a>
109
+ <a class="hero-feat-badge" href="https://neuronpedia.org/gemma-2-2b/10-gemmascope-res-16k/12435" target="_blank" rel="noopener">
110
+ <span class="feat-id">L10/12435</span>
111
+ <span class="feat-desc">specific locations or geographic information</span>
112
+ </a>
113
+ </div>
114
+ </section>
115
+
116
+ <header class="meta" aria-label="Article meta information">
117
+ <div class="meta-container">
118
+ {
119
+ normalizedAuthors.length > 0 && (
120
+ <div class="meta-container-cell">
121
+ <h3>Author{normalizedAuthors.length > 1 ? "s" : ""}</h3>
122
+ <ul class="authors">
123
+ {normalizedAuthors.map((a, i) => {
124
+ const supers =
125
+ shouldShowAffiliationSupers &&
126
+ Array.isArray(a.affiliationIndices) &&
127
+ a.affiliationIndices.length ? (
128
+ <sup>{a.affiliationIndices.join(", ")}</sup>
129
+ ) : null;
130
+ return (
131
+ <li>
132
+ {a.url ? <a href={a.url}>{a.name}</a> : a.name}{supers}{i < normalizedAuthors.length - 1 && <span set:html=",&nbsp;" />}
133
+ </li>
134
+ );
135
+ })}
136
+ </ul>
137
+ </div>
138
+ )
139
+ }
140
+ {
141
+ Array.isArray(affiliations) && affiliations.length > 0 && (
142
+ <div class="meta-container-cell meta-container-cell--affiliations">
143
+ <h3>Affiliation{affiliations.length > 1 ? "s" : ""}</h3>
144
+ {hasMultipleAffiliations ? (
145
+ <ol class="affiliations">
146
+ {affiliations.map((af) => (
147
+ <li value={af.id}>
148
+ {af.url ? (
149
+ <a href={af.url} target="_blank" rel="noopener noreferrer">
150
+ {af.name}
151
+ </a>
152
+ ) : (
153
+ af.name
154
+ )}
155
+ </li>
156
+ ))}
157
+ </ol>
158
+ ) : (
159
+ <p>
160
+ {affiliations[0]?.url ? (
161
+ <a
162
+ href={affiliations[0].url}
163
+ target="_blank"
164
+ rel="noopener noreferrer"
165
+ >
166
+ {affiliations[0].name}
167
+ </a>
168
+ ) : (
169
+ affiliations[0]?.name
170
+ )}
171
+ </p>
172
+ )}
173
+ </div>
174
+ )
175
+ }
176
+ {
177
+ (!affiliations || affiliations.length === 0) && affiliation && (
178
+ <div class="meta-container-cell meta-container-cell--affiliations">
179
+ <h3>Affiliation</h3>
180
+ <p>{affiliation}</p>
181
+ </div>
182
+ )
183
+ }
184
+ {
185
+ published && (
186
+ <div class="meta-container-cell meta-container-cell--published">
187
+ <h3>Published</h3>
188
+ <p>{published}</p>
189
+ </div>
190
+ )
191
+ }
192
+ <!-- {doi && (
193
+ <div class="meta-container-cell">
194
+ <h3>DOI</h3>
195
+ <p><a href={`https://doi.org/${doi}`} target="_blank" rel="noopener noreferrer">{doi}</a></p>
196
+ </div>
197
+ )} -->
198
+ <div class="meta-container-cell meta-container-cell--pdf">
199
+ <div class="pdf-header-wrapper">
200
+ <h3>PDF</h3>
201
+ <span class="pro-badge-wrapper" style="display: none;">
202
+ <span class="pro-badge-prefix">- you are</span>
203
+ <span class="pro-badge">PRO</span>
204
+ </span>
205
+ <span class="pro-only-label" style="display: none;">
206
+ <span class="pro-only-dash">-</span>
207
+ <span class="pro-only-text">pro only</span>
208
+ <svg class="pro-only-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
209
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
210
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
211
+ </svg>
212
+ </span>
213
+ </div>
214
+ <div id="pdf-download-container" data-pdf-pro-only={pdfProOnly.toString()}>
215
+ <p class="pdf-loading">Checking access...</p>
216
+ <p class="pdf-pro-only" style="display: none;">
217
+ <a
218
+ class="button"
219
+ href={arxiv ? `https://arxiv.org/pdf/${arxiv}` : `/${pdfFilename}`}
220
+ target={arxiv ? "_blank" : undefined}
221
+ rel={arxiv ? "noopener noreferrer" : undefined}
222
+ download={arxiv ? undefined : pdfFilename}
223
+ aria-label={arxiv ? `View PDF on arXiv` : `Download PDF ${pdfFilename}`}
224
+ >
225
+ Download PDF
226
+ </a>
227
+ </p>
228
+ <div class="pdf-locked" style="display: none;">
229
+ <a
230
+ class="button button-locked"
231
+ href="https://huggingface.co/subscribe/pro"
232
+ target="_blank"
233
+ rel="noopener noreferrer"
234
+ >
235
+ <svg class="lock-icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" viewBox="0 0 12 12" fill="none">
236
+ <path d="M6.48 1.26c0 1.55.67 2.58 1.5 3.24.86.68 1.9 1 2.58 1.07v.86A5.3 5.3 0 0 0 7.99 7.5a3.95 3.95 0 0 0-1.51 3.24h-.96c0-1.55-.67-2.58-1.5-3.24a5.3 5.3 0 0 0-2.58-1.07v-.86A5.3 5.3 0 0 0 4.01 4.5a3.95 3.95 0 0 0 1.51-3.24h.96Z" fill="currentColor"></path>
237
+ </svg>
238
+ <span class="locked-title">Subscribe to Pro</span>
239
+ </a>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </div>
244
+ </header>
245
+
246
+ <script>
247
+ // PDF access control for Pro users only
248
+
249
+ // βš™οΈ Configuration for local development
250
+ const LOCAL_IS_PRO = false; // Set to true to test Pro access locally
251
+
252
+ const FALLBACK_TIMEOUT_MS = 3000;
253
+ let userPlanChecked = false;
254
+
255
+ // Check if PDF Pro gating is enabled
256
+ const pdfContainer = document.querySelector("#pdf-download-container") as HTMLElement;
257
+ const pdfProOnly = pdfContainer?.getAttribute("data-pdf-pro-only") === "true";
258
+
259
+ /**
260
+ * Check if user has Pro access
261
+ * Isolated logic for Pro user verification
262
+ * Expected plan structure: { user: "pro", org: "enterprise" }
263
+ */
264
+ function isProUser(plan: any): boolean {
265
+ if (!plan) return false;
266
+
267
+ // Check if user property is "pro"
268
+ return plan.user === "pro";
269
+ }
270
+
271
+ /**
272
+ * Update UI based on user's Pro status
273
+ */
274
+ function updatePdfAccess(isPro: boolean) {
275
+ const loadingEl = document.querySelector(".pdf-loading") as HTMLElement;
276
+ const proOnlyEl = document.querySelector(".pdf-pro-only") as HTMLElement;
277
+ const lockedEl = document.querySelector(".pdf-locked") as HTMLElement;
278
+ const proOnlyLabel = document.querySelector(".pro-only-label") as HTMLElement;
279
+ const proBadgeWrapper = document.querySelector(".pro-badge-wrapper") as HTMLElement;
280
+
281
+ // Hide loading state
282
+ if (loadingEl) loadingEl.style.display = "none";
283
+
284
+ // If PDF Pro gating is disabled, just show the download button
285
+ if (!pdfProOnly) {
286
+ if (proOnlyEl) proOnlyEl.style.display = "block";
287
+ if (proOnlyLabel) proOnlyLabel.style.display = "none";
288
+ if (lockedEl) lockedEl.style.display = "none";
289
+ if (proBadgeWrapper) proBadgeWrapper.style.display = "none";
290
+ return;
291
+ }
292
+
293
+ // Show appropriate state based on Pro status
294
+ if (isPro) {
295
+ if (proOnlyEl) proOnlyEl.style.display = "block";
296
+ if (proOnlyLabel) proOnlyLabel.style.display = "none";
297
+ if (lockedEl) lockedEl.style.display = "none";
298
+ if (proBadgeWrapper) proBadgeWrapper.style.display = "inline-flex";
299
+ } else {
300
+ if (proOnlyEl) proOnlyEl.style.display = "none";
301
+ if (proOnlyLabel) proOnlyLabel.style.display = "inline-flex";
302
+ if (lockedEl) lockedEl.style.display = "block";
303
+ if (proBadgeWrapper) proBadgeWrapper.style.display = "none";
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Handle user plan response
309
+ */
310
+ function handleUserPlan(plan: any) {
311
+ userPlanChecked = true;
312
+ const isPro = isProUser(plan);
313
+ updatePdfAccess(isPro);
314
+
315
+ // Optional: log for debugging
316
+ console.log("[PDF Access]", { plan, isPro });
317
+ }
318
+
319
+ /**
320
+ * Fallback behavior when no parent window responds
321
+ * Uses LOCAL_IS_PRO configuration for local development
322
+ */
323
+ function handleFallback() {
324
+ if (LOCAL_IS_PRO) {
325
+ handleUserPlan({ user: "pro" });
326
+ } else {
327
+ handleUserPlan({ user: "free" });
328
+ }
329
+ }
330
+
331
+ // If PDF Pro gating is disabled, show the download button immediately
332
+ if (!pdfProOnly) {
333
+ updatePdfAccess(true);
334
+ } else {
335
+ // Listen for messages from parent window (Hugging Face Spaces)
336
+ window.addEventListener("message", (event) => {
337
+ if (event.data.type === "USER_PLAN") {
338
+ handleUserPlan(event.data.plan);
339
+ }
340
+ });
341
+
342
+ // Request user plan on page load
343
+ if (window.parent && window.parent !== window) {
344
+ // We're in an iframe, request user plan
345
+ window.parent.postMessage({ type: "USER_PLAN_REQUEST" }, "*");
346
+
347
+ // Fallback if no response after timeout
348
+ setTimeout(() => {
349
+ if (!userPlanChecked) {
350
+ handleFallback();
351
+ }
352
+ }, FALLBACK_TIMEOUT_MS);
353
+ } else {
354
+ // Not in iframe (local development), use fallback immediately
355
+ handleFallback();
356
+ }
357
+ }
358
+ </script>
359
+
360
+ <style>
361
+ /* Hero (full-width) */
362
+ .hero {
363
+ width: 100%;
364
+ padding: 64px 16px 16px;
365
+ text-align: center;
366
+ }
367
+ .hero-title {
368
+ font-size: clamp(28px, 4vw, 48px);
369
+ font-weight: 800;
370
+ line-height: 1.1;
371
+ margin: 0 0 28px;
372
+ max-width: 100%;
373
+ }
374
+ .hero-banner {
375
+ max-width: 1200px;
376
+ margin: 0 auto;
377
+ }
378
+ .hero-desc {
379
+ color: var(--muted-color);
380
+ font-style: italic;
381
+ margin: 0 auto 16px;
382
+ max-width: 55%;
383
+ }
384
+
385
+ @media (max-width: 768px) {
386
+ .hero-desc {
387
+ max-width: 90%;
388
+ }
389
+ }
390
+
391
+ /* Meta (byline-like header) */
392
+ .meta {
393
+ border-top: 1px solid var(--border-color);
394
+ border-bottom: 1px solid var(--border-color);
395
+ padding: 1rem 0;
396
+ font-size: 0.9rem;
397
+ }
398
+ .meta-container {
399
+ max-width: 980px;
400
+ display: flex;
401
+ flex-direction: row;
402
+ justify-content: space-between;
403
+ margin: 0 auto;
404
+ padding: 0 var(--content-padding-x);
405
+ gap: 8px;
406
+ }
407
+ /* Subtle underline for links in meta; keep buttons without underline */
408
+ .meta-container a:not(.button) {
409
+ color: var(--primary-color);
410
+ text-decoration: underline;
411
+ text-underline-offset: 2px;
412
+ text-decoration-thickness: 0.06em;
413
+ text-decoration-color: var(--link-underline);
414
+ transition: text-decoration-color 0.15s ease-in-out;
415
+ }
416
+ .meta-container a:hover {
417
+ text-decoration-color: var(--link-underline-hover);
418
+ }
419
+ .meta-container a.button,
420
+ .meta-container .button {
421
+ text-decoration: none;
422
+ }
423
+ .meta-container-cell {
424
+ display: flex;
425
+ flex-direction: column;
426
+ gap: 8px;
427
+ max-width: 400px;
428
+ }
429
+ .meta-container-cell h3 {
430
+ margin: 0;
431
+ font-size: 12px;
432
+ font-weight: 400;
433
+ color: var(--muted-color);
434
+ text-transform: uppercase;
435
+ letter-spacing: 0.02em;
436
+ }
437
+ .meta-container-cell p {
438
+ margin: 0;
439
+ }
440
+ .authors {
441
+ margin: 0;
442
+ list-style-type: none;
443
+ padding-left: 0;
444
+ display: flex;
445
+ flex-wrap: wrap;
446
+ }
447
+ .authors li {
448
+ white-space: nowrap;
449
+ padding:0;
450
+ }
451
+ .affiliations {
452
+ margin: 0;
453
+ padding-left: 1.25em;
454
+ }
455
+ .affiliations li {
456
+ margin: 0;
457
+ }
458
+
459
+ header.meta .meta-container {
460
+ flex-wrap: wrap;
461
+ row-gap: 12px;
462
+ }
463
+
464
+ @media (max-width: 768px) {
465
+ .meta-container-cell--affiliations,
466
+ .meta-container-cell--pdf {
467
+ text-align: right;
468
+ }
469
+ }
470
+
471
+ @media print {
472
+ .meta-container-cell--pdf {
473
+ display: none !important;
474
+ }
475
+ }
476
+
477
+ /* PDF access control styles */
478
+ .pdf-header-wrapper {
479
+ display: flex;
480
+ align-items: center;
481
+ gap: 6px;
482
+ line-height: 1;
483
+ }
484
+
485
+ .pdf-header-wrapper h3 {
486
+ line-height: 1;
487
+ }
488
+
489
+ .pdf-loading {
490
+ color: var(--muted-color);
491
+ font-size: 0.9em;
492
+ }
493
+
494
+ .pdf-pro-only {
495
+ margin: 0;
496
+ }
497
+
498
+ .pro-badge-wrapper {
499
+ display: inline-flex;
500
+ align-items: center;
501
+ gap: 5px;
502
+ font-style: normal;
503
+ }
504
+
505
+ .pro-badge-prefix {
506
+ font-size: 0.85em;
507
+ opacity: 0.5;
508
+ font-weight: 400;
509
+ font-style: normal;
510
+ }
511
+
512
+ .pro-badge {
513
+ display: inline-block;
514
+ border: 1px solid rgba(0, 0, 0, 0.025);
515
+ background: linear-gradient(to bottom right, #f9a8d4, #86efac, #fde047);
516
+ color: black;
517
+ padding: 1px 5px;
518
+ border-radius: 3px;
519
+ font-size: 0.5rem;
520
+ font-weight: 700;
521
+ font-style: normal;
522
+ letter-spacing: 0.025em;
523
+ text-transform: uppercase;
524
+ }
525
+
526
+ /* Dark mode pro badge */
527
+ :global(.dark) .pro-badge,
528
+ :global([data-theme="dark"]) .pro-badge {
529
+ background: linear-gradient(to bottom right, #ec4899, #22c55e, #eab308);
530
+ border-color: rgba(255, 255, 255, 0.15);
531
+ }
532
+
533
+ .pro-only-label {
534
+ display: inline-flex;
535
+ flex-direction: row;
536
+ align-items: center;
537
+ gap: 5px;
538
+ font-size: 0.85em;
539
+ opacity: 0.5;
540
+ font-weight: 400;
541
+ line-height: 1;
542
+ }
543
+
544
+ .pro-only-dash {
545
+ display: inline-flex;
546
+ align-items: center;
547
+ line-height: 1;
548
+ }
549
+
550
+ .pro-only-icon {
551
+ width: 11px;
552
+ height: 11px;
553
+ flex-shrink: 0;
554
+ display: inline-flex;
555
+ align-items: center;
556
+ }
557
+
558
+ .pro-only-text {
559
+ display: inline-flex;
560
+ align-items: center;
561
+ line-height: 1;
562
+ }
563
+
564
+ .pdf-locked {
565
+ display: block;
566
+ }
567
+
568
+ .button-locked {
569
+ display: inline-flex;
570
+ align-items: center;
571
+ gap: 6px;
572
+ background: linear-gradient(135deg,
573
+ var(--primary-color) 0%,
574
+ oklch(from var(--primary-color) calc(l - 0.1) calc(c + 0.05) calc(h - 60)) 100%);
575
+ border-radius: var(--button-radius);
576
+ padding: var(--button-padding-y) var(--button-padding-x);
577
+ font-size: var(--button-font-size);
578
+ line-height: 1;
579
+ color: var(--on-primary);
580
+ position: relative;
581
+ overflow: hidden;
582
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
583
+ font-weight: normal;
584
+ border-color: rgba(0, 0, 0, 0.15);
585
+ }
586
+
587
+ .button-locked:active {
588
+ transform: translateY(0);
589
+ }
590
+
591
+ .lock-icon {
592
+ font-size: 1em;
593
+ flex-shrink: 0;
594
+ position: relative;
595
+ z-index: 1;
596
+ }
597
+
598
+ .locked-title {
599
+ position: relative;
600
+ z-index: 1;
601
+ }
602
+
603
+ /* Dark mode locked button - inherits from light mode variables */
604
+
605
+ @media (max-width: 768px) {
606
+ .meta-container-cell--pdf {
607
+ display: flex;
608
+ flex-direction: column;
609
+ align-items: flex-end;
610
+ }
611
+ }
612
+ </style>
app/src/components/HtmlEmbed.astro ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ interface Props {
3
+ src: string;
4
+ title?: string;
5
+ desc?: string;
6
+ caption?: string;
7
+ frameless?: boolean;
8
+ align?: "left" | "center" | "right";
9
+ id?: string;
10
+ data?: string | string[];
11
+ config?: any;
12
+ }
13
+ const {
14
+ src,
15
+ title,
16
+ desc,
17
+ caption,
18
+ frameless = false,
19
+ align = "left",
20
+ id,
21
+ data,
22
+ config,
23
+ } = Astro.props as Props;
24
+
25
+ // Load all .html embeds under src/content/embeds/** as strings (dev & build)
26
+ const embeds = (import.meta as any).glob("../content/embeds/**/*.html", {
27
+ query: "?raw",
28
+ import: "default",
29
+ eager: true,
30
+ }) as Record<string, string>;
31
+
32
+ function resolveFragment(requested: string): string | null {
33
+ // Allow both "banner.html" and "embeds/banner.html"
34
+ const needle = requested.replace(/^\/*/, "");
35
+ for (const [key, html] of Object.entries(embeds)) {
36
+ if (
37
+ key.endsWith("/" + needle) ||
38
+ key.endsWith("/" + needle.replace(/^embeds\//, ""))
39
+ ) {
40
+ return html;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+
46
+ const html = resolveFragment(src);
47
+ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
48
+ const dataAttr = Array.isArray(data)
49
+ ? JSON.stringify(data)
50
+ : typeof data === "string"
51
+ ? data
52
+ : undefined;
53
+ const configAttr =
54
+ typeof config === "string"
55
+ ? config
56
+ : config != null
57
+ ? JSON.stringify(config)
58
+ : undefined;
59
+
60
+ // Apply the ID to the HTML content if provided
61
+ const htmlWithId =
62
+ id && html
63
+ ? html.replace(/<div class="([^"]*)"[^>]*>/, `<div class="$1" id="${id}">`)
64
+ : html;
65
+ ---
66
+
67
+ {
68
+ html ? (
69
+ <figure class="html-embed" id={id}>
70
+ {title && (
71
+ <figcaption class="html-embed__title" style={`text-align:${align}`}>
72
+ {title}
73
+ </figcaption>
74
+ )}
75
+ <div class={`html-embed__card${frameless ? " is-frameless" : ""}`}>
76
+ <div
77
+ id={mountId}
78
+ data-datafiles={dataAttr}
79
+ data-config={configAttr}
80
+ set:html={htmlWithId}
81
+ />
82
+ </div>
83
+ {(desc || caption) && (
84
+ <figcaption
85
+ class="html-embed__desc"
86
+ style={`text-align:${align}`}
87
+ set:html={caption || desc}
88
+ />
89
+ )}
90
+ </figure>
91
+ ) : (
92
+ <figure class="html-embed html-embed--error" id={id}>
93
+ <div class="html-embed__card html-embed__card--error">
94
+ <div class="html-embed__error">
95
+ <strong>Embed not found</strong>
96
+ <p>
97
+ The requested embed could not be loaded: <code>{src}</code>
98
+ </p>
99
+ </div>
100
+ </div>
101
+ </figure>
102
+ )
103
+ }
104
+
105
+ <script>
106
+ // Re-execute <script> tags inside the injected fragment (innerHTML doesn't run scripts)
107
+ const scriptEl = document.currentScript;
108
+ const mount = scriptEl ? scriptEl.previousElementSibling : null;
109
+ const execute = () => {
110
+ if (!mount) return;
111
+ const scripts = mount.querySelectorAll("script");
112
+ scripts.forEach((old) => {
113
+ // ignore non-executable types (e.g., application/json)
114
+ if (
115
+ old.type &&
116
+ old.type !== "text/javascript" &&
117
+ old.type !== "module" &&
118
+ old.type !== ""
119
+ )
120
+ return;
121
+ if (old.dataset.executed === "true") return;
122
+ old.dataset.executed = "true";
123
+ if (old.src) {
124
+ const s = document.createElement("script");
125
+ Array.from(old.attributes).forEach((attr) =>
126
+ s.setAttribute(attr.name, attr.value),
127
+ );
128
+ document.body.appendChild(s);
129
+ } else {
130
+ try {
131
+ // run inline
132
+ (0, eval)(old.text || "");
133
+ } catch (e) {
134
+ console.error("HtmlEmbed inline script error:", e);
135
+ }
136
+ }
137
+ });
138
+ };
139
+ // Execute after DOM is parsed (ensures deferred module scripts are executed first)
140
+ if (document.readyState === "loading")
141
+ document.addEventListener("DOMContentLoaded", execute, { once: true });
142
+ else execute();
143
+ </script>
144
+
145
+ <style is:global>
146
+ .html-embed {
147
+ margin: 0 0 var(--block-spacing-y);
148
+ z-index: var(--z-elevated);
149
+ position: relative;
150
+ }
151
+ .html-embed__title {
152
+ text-align: left;
153
+ font-weight: 600;
154
+ font-size: 0.95rem;
155
+ color: var(--text-color);
156
+ margin: 0;
157
+ padding: 0;
158
+ padding-bottom: var(--spacing-1);
159
+ position: relative;
160
+ display: block;
161
+ width: 100%;
162
+ background: var(--page-bg);
163
+ z-index: var(--z-elevated);
164
+ }
165
+ .html-embed__card {
166
+ background-color: var(--surface-bg);
167
+ border: 1px solid var(--border-color);
168
+ border-radius: 10px;
169
+ padding: 24px;
170
+ z-index: calc(var(--z-elevated) + 1);
171
+ position: relative;
172
+ }
173
+ .html-embed__card.is-frameless {
174
+ background: transparent;
175
+ border: none;
176
+ padding: 0;
177
+ }
178
+ .html-embed__desc {
179
+ text-align: left;
180
+ font-size: 0.9rem;
181
+ color: var(--muted-color);
182
+ margin: 0;
183
+ padding: 0;
184
+ padding-top: var(--spacing-1);
185
+ position: relative;
186
+ z-index: var(--z-elevated);
187
+ display: block;
188
+ width: 100%;
189
+ background: var(--page-bg);
190
+ }
191
+ /* Error state for missing embeds */
192
+ .html-embed__card--error {
193
+ background: #fef2f2;
194
+ border: 2px solid #dc2626;
195
+ border-radius: 8px;
196
+ padding: 20px;
197
+ }
198
+ .html-embed__error {
199
+ text-align: center;
200
+ color: #dc2626;
201
+ }
202
+ .html-embed__error strong {
203
+ display: block;
204
+ font-size: 1.1rem;
205
+ font-weight: 600;
206
+ margin-bottom: 8px;
207
+ }
208
+ .html-embed__error p {
209
+ margin: 0;
210
+ font-size: 0.9rem;
211
+ line-height: 1.5;
212
+ }
213
+ .html-embed__error code {
214
+ background: rgba(220, 38, 38, 0.1);
215
+ padding: 2px 6px;
216
+ border-radius: 4px;
217
+ font-family: var(--font-mono);
218
+ font-size: 0.85rem;
219
+ word-break: break-all;
220
+ }
221
+ /* Dark mode for error state */
222
+ [data-theme="dark"] .html-embed__card--error {
223
+ background: #1f2937;
224
+ border-color: #ef4444;
225
+ }
226
+ [data-theme="dark"] .html-embed__error {
227
+ color: #ef4444;
228
+ }
229
+ [data-theme="dark"] .html-embed__error code {
230
+ background: rgba(239, 68, 68, 0.2);
231
+ }
232
+ /* Plotly – fragments & controls */
233
+ .html-embed__card svg text {
234
+ fill: var(--text-color);
235
+ }
236
+ .html-embed__card label {
237
+ color: var(--text-color);
238
+ }
239
+ .plotly-graph-div {
240
+ width: 100%;
241
+ min-height: 320px;
242
+ }
243
+ @media (max-width: 768px) {
244
+ .plotly-graph-div {
245
+ min-height: 260px;
246
+ }
247
+ }
248
+ [id^="plot-"] {
249
+ display: flex;
250
+ flex-direction: column;
251
+ align-items: center;
252
+ gap: 15px;
253
+ }
254
+ .plotly_caption {
255
+ font-style: italic;
256
+ margin-top: 10px;
257
+ }
258
+ .plotly_controls {
259
+ display: flex;
260
+ flex-wrap: wrap;
261
+ justify-content: center;
262
+ gap: 30px;
263
+ }
264
+ .plotly_input_container {
265
+ display: flex;
266
+ align-items: center;
267
+ flex-direction: column;
268
+ gap: 10px;
269
+ }
270
+ .plotly_input_container > select {
271
+ padding: 2px 4px;
272
+ line-height: 1.5em;
273
+ text-align: center;
274
+ border-radius: 4px;
275
+ font-size: 12px;
276
+ background-color: var(--neutral-200);
277
+ outline: none;
278
+ border: 1px solid var(--neutral-300);
279
+ }
280
+ .plotly_slider {
281
+ display: flex;
282
+ align-items: center;
283
+ gap: 10px;
284
+ }
285
+ .plotly_slider > input[type="range"] {
286
+ -webkit-appearance: none;
287
+ appearance: none;
288
+ height: 2px;
289
+ background: var(--neutral-400);
290
+ border-radius: 5px;
291
+ outline: none;
292
+ }
293
+ .plotly_slider > input[type="range"]::-webkit-slider-thumb {
294
+ -webkit-appearance: none;
295
+ width: 18px;
296
+ height: 18px;
297
+ border-radius: 50%;
298
+ background: var(--primary-color);
299
+ cursor: pointer;
300
+ }
301
+ .plotly_slider > input[type="range"]::-moz-range-thumb {
302
+ width: 18px;
303
+ height: 18px;
304
+ border-radius: 50%;
305
+ background: var(--primary-color);
306
+ cursor: pointer;
307
+ }
308
+ .plotly_slider > span {
309
+ font-size: 14px;
310
+ line-height: 1.6em;
311
+ min-width: 16px;
312
+ }
313
+ /* Dark mode overrides for Plotly readability */
314
+ [data-theme="dark"] .html-embed__card .xaxislayer-above text,
315
+ [data-theme="dark"] .html-embed__card .yaxislayer-above text,
316
+ [data-theme="dark"] .html-embed__card .infolayer text,
317
+ [data-theme="dark"] .html-embed__card .legend text,
318
+ [data-theme="dark"] .html-embed__card .annotation text,
319
+ [data-theme="dark"] .html-embed__card .colorbar text,
320
+ [data-theme="dark"] .html-embed__card .hoverlayer text {
321
+ fill: #fff !important;
322
+ }
323
+ [data-theme="dark"] .html-embed__card .xaxislayer-above path,
324
+ [data-theme="dark"] .html-embed__card .yaxislayer-above path,
325
+ [data-theme="dark"] .html-embed__card .xlines-above,
326
+ [data-theme="dark"] .html-embed__card .ylines-above {
327
+ stroke: rgba(255, 255, 255, 0.35) !important;
328
+ }
329
+ [data-theme="dark"] .html-embed__card .gridlayer path {
330
+ stroke: rgba(255, 255, 255, 0.15) !important;
331
+ }
332
+ [data-theme="dark"] .html-embed__card .legend rect.bg {
333
+ fill: rgba(0, 0, 0, 0.25) !important;
334
+ stroke: rgba(255, 255, 255, 0.2) !important;
335
+ }
336
+ [data-theme="dark"] .html-embed__card .hoverlayer .bg {
337
+ fill: rgba(0, 0, 0, 0.8) !important;
338
+ stroke: rgba(255, 255, 255, 0.2) !important;
339
+ }
340
+ [data-theme="dark"] .html-embed__card .colorbar .cbbg {
341
+ fill: rgba(0, 0, 0, 0.25) !important;
342
+ stroke: rgba(255, 255, 255, 0.2) !important;
343
+ }
344
+ @media print {
345
+ .html-embed,
346
+ .html-embed__card {
347
+ max-width: 100% !important;
348
+ width: 100% !important;
349
+ margin-left: 0 !important;
350
+ margin-right: 0 !important;
351
+ }
352
+ .html-embed__card {
353
+ padding: 6px;
354
+ }
355
+ .html-embed__card.is-frameless {
356
+ padding: 0;
357
+ }
358
+ .html-embed__card svg,
359
+ .html-embed__card canvas,
360
+ .html-embed__card img {
361
+ max-width: 100% !important;
362
+ height: auto !important;
363
+ }
364
+ .html-embed__card > div[id^="frag-"] {
365
+ width: 100% !important;
366
+ }
367
+ }
368
+ @media print {
369
+ /* Avoid breaks inside embeds */
370
+ .html-embed,
371
+ .html-embed__card {
372
+ break-inside: avoid;
373
+ page-break-inside: avoid;
374
+ }
375
+ /* Constrain width and scale inner content */
376
+ .html-embed,
377
+ .html-embed__card {
378
+ max-width: 100% !important;
379
+ width: 100% !important;
380
+ }
381
+ .html-embed__card {
382
+ padding: 6px;
383
+ }
384
+ .html-embed__card.is-frameless {
385
+ padding: 0;
386
+ }
387
+ .html-embed__card svg,
388
+ .html-embed__card canvas,
389
+ .html-embed__card img,
390
+ .html-embed__card video,
391
+ .html-embed__card iframe {
392
+ max-width: 100% !important;
393
+ height: auto !important;
394
+ }
395
+ .html-embed__card > div[id^="frag-"] {
396
+ width: 100% !important;
397
+ max-width: 100% !important;
398
+ }
399
+ /* Center and constrain the banner (galaxy) when printing */
400
+ .html-embed .d3-galaxy {
401
+ width: 100% !important;
402
+ max-width: 980px !important;
403
+ margin-left: auto !important;
404
+ margin-right: auto !important;
405
+ }
406
+ }
407
+ </style>
app/src/components/Note.astro ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ interface Props {
3
+ title?: string;
4
+ emoji?: string;
5
+ class?: string;
6
+ variant?: "neutral" | "info" | "success" | "danger";
7
+ }
8
+ const {
9
+ title,
10
+ emoji,
11
+ class: className,
12
+ variant = "neutral",
13
+ ...props
14
+ } = Astro.props as Props;
15
+ const wrapperClass = ["note", `note--${variant}`, className]
16
+ .filter(Boolean)
17
+ .join(" ");
18
+ const hasHeader =
19
+ (emoji && String(emoji).length > 0) || (title && String(title).length > 0);
20
+ ---
21
+
22
+ <div class={wrapperClass} {...props}>
23
+ {title ? (
24
+ <!-- When there's a title, emoji is inline with title -->
25
+ <div class="note__body">
26
+ <div class="note__header">
27
+ {emoji && <span class="note__emoji-inline" aria-hidden="true">{emoji}</span>}
28
+ <div class="note__title">{title}</div>
29
+ </div>
30
+ <div class="note__content">
31
+ <slot />
32
+ </div>
33
+ </div>
34
+ ) : (
35
+ <!-- When there's no title, emoji is above content -->
36
+ <div class="note__layout">
37
+ {
38
+ emoji && (
39
+ <div class="note__icon" aria-hidden="true">
40
+ <span class="note__emoji">{emoji}</span>
41
+ </div>
42
+ )
43
+ }
44
+ <div class="note__body">
45
+ <div class="note__content">
46
+ <slot />
47
+ </div>
48
+ </div>
49
+ </div>
50
+ )}
51
+ </div>
52
+
53
+ <style>
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
+ }
61
+ .note__layout {
62
+ display: flex;
63
+ align-items: flex-start;
64
+ gap: 10px;
65
+ }
66
+ .note__icon {
67
+ flex: 0 0 auto;
68
+ line-height: 1;
69
+ padding-top: 2px;
70
+ }
71
+ .note__emoji {
72
+ font-size: 32px;
73
+ line-height: 1;
74
+ display: block;
75
+ }
76
+ .note__body {
77
+ flex: 1 1 auto;
78
+ min-width: 0;
79
+ }
80
+ .note__header {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 8px;
84
+ margin-bottom: 6px;
85
+ }
86
+ .note__emoji-inline {
87
+ font-size: 20px;
88
+ line-height: 1;
89
+ flex-shrink: 0;
90
+ }
91
+ .note__title {
92
+ font-size: 16px;
93
+ letter-spacing: 0.2px;
94
+ font-weight: 600;
95
+ color: var(--text-color);
96
+ text-align: left;
97
+ }
98
+ .note__content {
99
+ color: var(--text-color);
100
+ font-size: 0.95rem;
101
+ text-align: left;
102
+ }
103
+ /* Ensure the very last slotted element has no bottom margin */
104
+ .note .note__content > :global(*:last-child) {
105
+ margin-bottom: 0 !important;
106
+ }
107
+
108
+ /* Variants */
109
+ .note.note--neutral {
110
+ border-left-color: var(--border-color);
111
+ background: var(--surface-bg);
112
+ }
113
+ .note.note--info {
114
+ border-left-color: #f39c12;
115
+ background: color-mix(in oklab, #f39c12 10%, var(--surface-bg));
116
+ }
117
+ .note.note--success {
118
+ border-left-color: #2ecc71;
119
+ background: color-mix(in oklab, #2ecc71 8%, var(--surface-bg));
120
+ }
121
+ .note.note--danger {
122
+ border-left-color: #e74c3c;
123
+ background: color-mix(in oklab, #e74c3c 8%, var(--surface-bg));
124
+ }
125
+ </style>
app/src/components/Quote.astro ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ interface Props {
3
+ author?: string;
4
+ source?: string;
5
+ }
6
+
7
+ const { author, source } = Astro.props;
8
+ ---
9
+
10
+ <blockquote class="quote">
11
+ <div class="quote__text">
12
+ <slot />
13
+ </div>
14
+ {
15
+ (author || source) && (
16
+ <footer class="quote__footer">
17
+ {author && <span class="quote__author" set:html={author} />}
18
+ {author && source && <span class="quote__separator">, </span>}
19
+ {source && <span class="quote__source" set:html={source} />}
20
+ </footer>
21
+ )
22
+ }
23
+ </blockquote>
24
+
25
+ <style>
26
+ .quote {
27
+ font-family: var(--default-font-family);
28
+ margin: 32px 0;
29
+ max-width: 600px;
30
+ text-align: left;
31
+ position: relative;
32
+ padding: 0;
33
+ border: none;
34
+ background: none;
35
+ box-shadow: none;
36
+ white-space: normal;
37
+ }
38
+
39
+ .quote::before {
40
+ content: '"';
41
+ position: absolute;
42
+ top: -24px;
43
+ left: -30px;
44
+ font-size: 8rem;
45
+ font-family: var(--default-font-family);
46
+ font-weight: 400;
47
+ color: var(--text-color);
48
+ opacity: 0.05;
49
+ z-index: -1;
50
+ line-height: 1;
51
+ pointer-events: none;
52
+ }
53
+
54
+ .quote__text {
55
+ font-size: 1.5rem;
56
+ line-height: 1.4;
57
+ font-style: normal;
58
+ font-weight: 400;
59
+ font-family:
60
+ "SF Pro Text",
61
+ -apple-system,
62
+ BlinkMacSystemFont,
63
+ "Segoe UI",
64
+ sans-serif;
65
+ color: var(--text-color);
66
+ margin-bottom: 12px;
67
+ padding: 0;
68
+ letter-spacing: -0.01em;
69
+ }
70
+
71
+ .quote__text p {
72
+ margin: 0;
73
+ }
74
+
75
+ .quote__footer {
76
+ font-size: 0.875rem;
77
+ color: var(--muted-color);
78
+ display: flex;
79
+ justify-content: flex-start;
80
+ align-items: center;
81
+ gap: 6px;
82
+ margin-top: 0;
83
+ font-family:
84
+ "SF Pro Text",
85
+ -apple-system,
86
+ BlinkMacSystemFont,
87
+ "Segoe UI",
88
+ sans-serif;
89
+ }
90
+
91
+ .quote__author {
92
+ font-weight: 500;
93
+ opacity: 0.85;
94
+ font-style: italic;
95
+ color: var(--text-color);
96
+ }
97
+
98
+ .quote__author::before {
99
+ content: "β€” ";
100
+ font-style: normal;
101
+ }
102
+
103
+ .quote__separator {
104
+ font-weight: 500;
105
+ opacity: 0.85;
106
+ color: var(--text-color);
107
+ }
108
+
109
+ .quote__source {
110
+ font-weight: 500;
111
+ opacity: 0.85;
112
+ font-style: italic;
113
+ color: var(--text-color);
114
+ }
115
+
116
+ @media (max-width: 640px) {
117
+ .quote {
118
+ margin: 24px 0;
119
+ max-width: 100%;
120
+ }
121
+
122
+ .quote::before {
123
+ font-size: 6rem;
124
+ top: -18px;
125
+ left: -16px;
126
+ }
127
+
128
+ .quote__text {
129
+ font-size: 1.25rem;
130
+ line-height: 1.45;
131
+ padding: 0;
132
+ }
133
+
134
+ .quote__footer {
135
+ flex-direction: row;
136
+ gap: 6px;
137
+ font-size: 0.8rem;
138
+ }
139
+ }
140
+ </style>
app/src/components/Reference.astro ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ interface Props {
3
+ /** ID unique pour la rΓ©fΓ©rence */
4
+ id: string;
5
+ /** LΓ©gende HTML pour la rΓ©fΓ©rence */
6
+ caption: string;
7
+ }
8
+
9
+ const { id, caption } = Astro.props as Props;
10
+ ---
11
+
12
+ <div class="reference-wrapper" id={id}>
13
+ <figure class="reference">
14
+ <div class="reference__content">
15
+ <slot />
16
+ </div>
17
+ <figcaption class="reference__caption" set:html={caption} />
18
+ </figure>
19
+ </div>
20
+
21
+ <style>
22
+ .reference-wrapper {
23
+ margin: 0;
24
+ }
25
+
26
+ .reference {
27
+ margin: 0;
28
+ }
29
+
30
+ .reference__content {
31
+ /* Le contenu peut Γͺtre n'importe quoi */
32
+ }
33
+
34
+ .reference__caption {
35
+ text-align: left;
36
+ font-size: 0.9rem;
37
+ color: var(--muted-color);
38
+ margin-top: 6px;
39
+ background: var(--page-bg);
40
+ position: relative;
41
+ z-index: var(--z-elevated);
42
+ display: block;
43
+ width: 100%;
44
+ }
45
+ </style>
app/src/components/Seo.astro ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ interface Props {
3
+ title: string;
4
+ description?: string;
5
+ authors?: string[];
6
+ published?: string;
7
+ tags?: string[];
8
+ image?: string; // Preferred absolute URL
9
+ }
10
+
11
+ const { title, description = '', authors = [], published, tags = [], image } = Astro.props as Props;
12
+
13
+ const url = Astro.url?.toString?.() ?? '';
14
+ const site = (Astro.site ? String(Astro.site) : '') as string;
15
+
16
+ const ogImage = image && image.length > 0
17
+ ? (image.startsWith('http') ? image : (site ? new URL(image, site).toString() : image))
18
+ : undefined;
19
+
20
+ const jsonLd = {
21
+ '@context': 'https://schema.org',
22
+ '@type': 'Article',
23
+ headline: title,
24
+ description: description || undefined,
25
+ datePublished: published || undefined,
26
+ author: authors.map((name) => ({ '@type': 'Person', name })),
27
+ keywords: tags.length ? tags.join(', ') : undefined,
28
+ mainEntityOfPage: url || undefined,
29
+ image: ogImage ? [ogImage] : undefined,
30
+ };
31
+ ---
32
+ <title>{title}</title>
33
+ <meta name="description" content={description} />
34
+ <link rel="canonical" href={url} />
35
+
36
+ <meta property="og:type" content="article" />
37
+ <meta property="og:title" content={title} />
38
+ {description && <meta property="og:description" content={description} />}
39
+ <meta property="og:url" content={url} />
40
+ {ogImage && <meta property="og:image" content={ogImage} />}
41
+ {published && <meta property="article:published_time" content={published} />}
42
+ {authors.map(a => <meta property="article:author" content={a} />)}
43
+
44
+ <meta name="twitter:card" content={ogImage ? 'summary_large_image' : 'summary'} />
45
+ <meta name="twitter:title" content={title} />
46
+ {description && <meta name="twitter:description" content={description} />}
47
+ {ogImage && <meta name="twitter:image" content={ogImage} />}
48
+
49
+ <script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
50
+
51
+
app/src/components/Sidenote.astro ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+
3
+ ---
4
+
5
+ <div class="sidenote-container">
6
+ <aside class="sidenote">
7
+ <slot />
8
+ </aside>
9
+ </div>
10
+
11
+ <script>
12
+ document.addEventListener("DOMContentLoaded", () => {
13
+ const containers = document.querySelectorAll(".sidenote-container");
14
+
15
+ containers.forEach((container) => {
16
+ // Find the previous element (sibling just before)
17
+ const previousElement = container.previousElementSibling as HTMLElement;
18
+
19
+ if (previousElement && previousElement.parentNode) {
20
+ // Create a wrapper div that will contain both the previous element and the sidenote
21
+ const wrapper = document.createElement("div");
22
+ wrapper.className = "sidenote-wrapper";
23
+
24
+ // Insert the wrapper before the previous element
25
+ previousElement.parentNode.insertBefore(wrapper, previousElement);
26
+
27
+ // Move both the previous element and the sidenote container into the wrapper
28
+ wrapper.appendChild(previousElement);
29
+ wrapper.appendChild(container);
30
+
31
+ // Style the wrapper to create the layout
32
+ wrapper.style.position = "relative";
33
+ wrapper.style.display = "block";
34
+
35
+ // Style the sidenote container so it positions correctly
36
+ const sidenoteContainer = container as HTMLElement;
37
+ sidenoteContainer.style.position = "absolute";
38
+ sidenoteContainer.style.top = "0";
39
+ sidenoteContainer.style.right = "-292px"; // 260px width + 32px gap
40
+ sidenoteContainer.style.width = "260px";
41
+
42
+ // Display the container with a fade-in
43
+ sidenoteContainer.style.display = "block";
44
+ sidenoteContainer.style.opacity = "0";
45
+
46
+ // Fade-in with transition
47
+ setTimeout(() => {
48
+ sidenoteContainer.style.opacity = "1";
49
+ }, 10);
50
+ }
51
+ });
52
+ });
53
+ </script>
54
+
55
+ <style is:global>
56
+ .sidenote-wrapper {
57
+ /* Le wrapper contient l'Γ©lΓ©ment original et le sidenote */
58
+ position: relative;
59
+ display: block;
60
+ }
61
+
62
+ .sidenote-container {
63
+ /* CachΓ© par dΓ©faut, sera affichΓ© par JS */
64
+ display: none;
65
+ margin: 0;
66
+ /* Transition for fade-in */
67
+ transition: opacity 0.3s ease-in-out;
68
+ }
69
+
70
+ .sidenote {
71
+ border-radius: 8px;
72
+ padding: 0 30px;
73
+ font-size: 0.9rem;
74
+ color: var(--muted-color);
75
+ margin: 0;
76
+ }
77
+
78
+ @media (--bp-content-collapse) {
79
+ .sidenote-wrapper {
80
+ /* Sur mobile, le wrapper n'a pas besoin de position relative */
81
+ position: static !important;
82
+ }
83
+
84
+ .sidenote-container {
85
+ position: static !important;
86
+ width: auto !important;
87
+ right: auto !important;
88
+ top: auto !important;
89
+ margin-top: 8px;
90
+ /* Affichage normal sur mobile */
91
+ display: block !important;
92
+ opacity: 1 !important;
93
+ }
94
+
95
+ .sidenote {
96
+ padding: 0;
97
+ }
98
+ }
99
+ </style>
app/src/components/TableOfContents.astro ADDED
@@ -0,0 +1,860 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ export interface Props {
3
+ tableOfContentAutoCollapse?: boolean;
4
+ }
5
+ const { tableOfContentAutoCollapse = false } = Astro.props as Props;
6
+ ---
7
+
8
+ <nav
9
+ class="table-of-contents"
10
+ aria-label="Table of Contents"
11
+ data-auto-collapse={tableOfContentAutoCollapse ? "1" : "0"}
12
+ >
13
+ <div class="title">Table of Contents</div>
14
+ <div id="article-toc-placeholder"></div>
15
+ </nav>
16
+ <details class="table-of-contents-mobile">
17
+ <summary>Table of Contents</summary>
18
+ <div id="article-toc-mobile-placeholder"></div>
19
+ </details>
20
+
21
+ <script is:inline>
22
+ // Build TOC from article headings (h2/h3/h4) and render into the sticky aside
23
+ const buildTOC = () => {
24
+ const holder = document.getElementById("article-toc-placeholder");
25
+ const holderMobile = document.getElementById(
26
+ "article-toc-mobile-placeholder",
27
+ );
28
+ // Always rebuild TOC to avoid stale entries
29
+ if (holder) holder.innerHTML = "";
30
+ if (holderMobile) holderMobile.innerHTML = "";
31
+ const articleRoot = document.querySelector("section.content-grid main");
32
+ if (!articleRoot) return;
33
+ const headings = articleRoot.querySelectorAll("h2, h3, h4");
34
+ if (!headings.length) return;
35
+
36
+ // Inclure tous les titres H2/H3/H4 sans filtrer "Table of contents"
37
+ const headingsArr = Array.from(headings);
38
+ if (!headingsArr.length) return;
39
+
40
+ // Ensure unique ids for headings (deduplicate duplicates)
41
+ const usedIds = new Set();
42
+ const slugify = (s) =>
43
+ String(s || "")
44
+ .toLowerCase()
45
+ .trim()
46
+ .replace(/\s+/g, "_")
47
+ .replace(/[^a-z0-9_\-]/g, "");
48
+ headingsArr.forEach((h) => {
49
+ let id = (h.id || "").trim();
50
+ if (!id) {
51
+ const base = slugify(h.textContent || "");
52
+ id = base || "section";
53
+ }
54
+ let candidate = id;
55
+ let n = 2;
56
+ while (usedIds.has(candidate)) {
57
+ candidate = `${id}-${n++}`;
58
+ }
59
+ if (h.id !== candidate) h.id = candidate;
60
+ usedIds.add(candidate);
61
+ });
62
+
63
+ const nav = document.createElement("nav");
64
+ let ulStack = [document.createElement("ul")];
65
+ nav.appendChild(ulStack[0]);
66
+
67
+ const levelOf = (tag) => (tag === "H2" ? 2 : tag === "H3" ? 3 : 4);
68
+ let prev = 2;
69
+ let headingCount = 0;
70
+ headingsArr.forEach((h) => {
71
+ const lvl = levelOf(h.tagName);
72
+ // adjust depth
73
+ while (lvl > prev) {
74
+ const ul = document.createElement("ul");
75
+ ulStack[ulStack.length - 1].lastElementChild?.appendChild(ul);
76
+ ulStack.push(ul);
77
+ prev++;
78
+ }
79
+ while (lvl < prev) {
80
+ ulStack.pop();
81
+ prev--;
82
+ }
83
+ const li = document.createElement("li");
84
+ const a = document.createElement("a");
85
+ a.href = "#" + h.id;
86
+ a.textContent = h.textContent;
87
+ a.target = "_self";
88
+ li.appendChild(a);
89
+ // Ajouter un index unique Γ  chaque heading pour le tracking
90
+ li.setAttribute("data-heading-idx", String(headingCount));
91
+ headingCount++;
92
+ ulStack[ulStack.length - 1].appendChild(li);
93
+ });
94
+
95
+ if (holder) holder.appendChild(nav);
96
+ const navClone = nav.cloneNode(true);
97
+ if (holderMobile) holderMobile.appendChild(navClone);
98
+
99
+ // active link on scroll
100
+ const links = [
101
+ ...(holder ? holder.querySelectorAll("a") : []),
102
+ ...(holderMobile ? holderMobile.querySelectorAll("a") : []),
103
+ ];
104
+ // Read breakpoint from CSS var and set autoCollapse only on desktop (disabled on mobile)
105
+ const getCollapsePx = () => {
106
+ const root = document.documentElement;
107
+ const raw = getComputedStyle(root)
108
+ .getPropertyValue("--bp-content-collapse")
109
+ .trim();
110
+ return raw || "1100px";
111
+ };
112
+ const mq = window.matchMedia(`(max-width: ${getCollapsePx()})`);
113
+ const attrEnabled =
114
+ document
115
+ .querySelector(".table-of-contents")
116
+ ?.getAttribute("data-auto-collapse") === "1";
117
+ let autoCollapse = attrEnabled && !mq.matches;
118
+
119
+ // Inject styles for collapsible & animation (tous les niveaux)
120
+ const ensureStyles = () => {
121
+ if (document.getElementById("toc-collapse-style")) return;
122
+ const style = document.createElement("style");
123
+ style.id = "toc-collapse-style";
124
+ style.textContent = `
125
+ .table-of-contents nav.table-of-contents-collapsible li > ul,
126
+ details.table-of-contents-mobile nav.table-of-contents-collapsible li > ul { overflow: hidden; transition: height 200ms ease; }
127
+ .table-of-contents nav.table-of-contents-collapsible li.collapsed > ul,
128
+ details.table-of-contents-mobile nav.table-of-contents-collapsible li.collapsed > ul { display: block; }
129
+ `;
130
+ document.head.appendChild(style);
131
+ };
132
+ ensureStyles();
133
+
134
+ const getAllItemsWithChildren = () => {
135
+ const sideNav = holder ? holder.querySelector("nav") : null;
136
+ const mobileNav = holderMobile ? holderMobile.querySelector("nav") : null;
137
+ const q = (navEl) =>
138
+ navEl
139
+ ? Array.from(navEl.querySelectorAll("li[data-heading-idx]")).filter(
140
+ (li) => li.querySelector(":scope > ul"),
141
+ )
142
+ : [];
143
+ return {
144
+ sideNav,
145
+ mobileNav,
146
+ sideItems: q(sideNav),
147
+ mobileItems: q(mobileNav),
148
+ };
149
+ };
150
+
151
+ const setNavCollapsible = () => {
152
+ const sideNav = holder ? holder.querySelector("nav") : null;
153
+ const mobileNav = holderMobile ? holderMobile.querySelector("nav") : null;
154
+ if (sideNav) sideNav.classList.add("table-of-contents-collapsible");
155
+ if (mobileNav) mobileNav.classList.add("table-of-contents-collapsible");
156
+ };
157
+
158
+ const measure = (el) => {
159
+ if (!el) return 0;
160
+ // Temporarily set height to auto to measure scrollHeight reliably
161
+ const prev = el.style.height;
162
+ el.style.height = "auto";
163
+
164
+ // Force un reflow pour que le navigateur calcule les wraps de texte
165
+ void el.offsetHeight;
166
+
167
+ // Maintenant scrollHeight inclut la vraie hauteur avec tous les line wraps
168
+ const h = el.scrollHeight;
169
+ el.style.height = prev || "";
170
+ return h;
171
+ };
172
+
173
+ // Tracker les animations en cours pour pouvoir les annuler
174
+ const activeAnimations = new Map();
175
+
176
+ const cancelAnimation = (el) => {
177
+ if (!el) return;
178
+ const animData = activeAnimations.get(el);
179
+ if (animData) {
180
+ // Nettoyer le listener de l'animation prΓ©cΓ©dente
181
+ el.removeEventListener("transitionend", animData.onEnd);
182
+ activeAnimations.delete(el);
183
+ }
184
+ };
185
+
186
+ const animateTo = (el, target) => {
187
+ if (!el) return;
188
+
189
+ // Annuler toute animation en cours sur cet Γ©lΓ©ment
190
+ cancelAnimation(el);
191
+
192
+ // Obtenir la hauteur ACTUELLE (mΓͺme si une animation est en cours)
193
+ const current = parseFloat(getComputedStyle(el).height) || 0;
194
+
195
+ // Si on est dΓ©jΓ  proche de la cible, pas besoin d'animer
196
+ if (Math.abs(current - target) < 1) {
197
+ el.style.height = target ? "auto" : "0px";
198
+ return;
199
+ }
200
+
201
+ // DΓ©marrer depuis la hauteur actuelle
202
+ el.style.height = current + "px";
203
+ // Force reflow
204
+ void el.offsetHeight;
205
+
206
+ // Aller vers la cible
207
+ el.style.height = target + "px";
208
+
209
+ // CrΓ©er le listener de fin
210
+ const onEnd = (e) => {
211
+ if (e.propertyName !== "height") return;
212
+ el.removeEventListener("transitionend", onEnd);
213
+ activeAnimations.delete(el);
214
+ if (target > 0) el.style.height = "auto";
215
+ };
216
+
217
+ // Sauvegarder le listener pour pouvoir l'annuler plus tard
218
+ activeAnimations.set(el, { onEnd });
219
+ el.addEventListener("transitionend", onEnd);
220
+ };
221
+
222
+ let prevActiveIdx = -1;
223
+ let prevActiveElements = new Set();
224
+ let prevActiveHeadingId = null;
225
+
226
+ const setCollapsedState = (activeIdx) => {
227
+ if (!autoCollapse) return;
228
+ if (activeIdx == null || activeIdx < 0) activeIdx = 0;
229
+ const { sideItems, mobileItems } = getAllItemsWithChildren();
230
+
231
+ // Trouver l'Γ©lΓ©ment <li> correspondant au heading actif et tous ses ancΓͺtres
232
+ const getActiveAndAncestors = (items, targetIdx) => {
233
+ const toExpand = new Set();
234
+
235
+ // Trouver le <li> qui correspond au targetIdx
236
+ const findActiveLi = (li) => {
237
+ const idx = Number(li.getAttribute("data-heading-idx") || "-1");
238
+ if (idx === targetIdx) {
239
+ return li;
240
+ }
241
+
242
+ const childUl = li.querySelector(":scope > ul");
243
+ if (!childUl) return null;
244
+
245
+ const childLis = childUl.querySelectorAll(
246
+ ":scope > li[data-heading-idx]",
247
+ );
248
+ for (const child of childLis) {
249
+ const found = findActiveLi(child);
250
+ if (found) return found;
251
+ }
252
+ return null;
253
+ };
254
+
255
+ let activeLi = null;
256
+ for (const li of items) {
257
+ activeLi = findActiveLi(li);
258
+ if (activeLi) break;
259
+ }
260
+
261
+ if (!activeLi) return toExpand;
262
+
263
+ // Collecter l'Γ©lΓ©ment actif lui-mΓͺme
264
+ const activeIdx = Number(
265
+ activeLi.getAttribute("data-heading-idx") || "-1",
266
+ );
267
+ toExpand.add(activeIdx);
268
+
269
+ // Remonter et collecter TOUS les ancΓͺtres, sans condition
270
+ // La structure de la TOC dΓ©termine automatiquement qui doit Γͺtre ouvert
271
+ let current = activeLi;
272
+ while (current) {
273
+ const parent = current.parentElement?.closest("li[data-heading-idx]");
274
+ if (parent) {
275
+ const parentIdx = Number(
276
+ parent.getAttribute("data-heading-idx") || "-1",
277
+ );
278
+ toExpand.add(parentIdx);
279
+ current = parent;
280
+ } else {
281
+ break;
282
+ }
283
+ }
284
+
285
+ return toExpand;
286
+ };
287
+
288
+ const update = (items) => {
289
+ const newActiveAncestors = getActiveAndAncestors(items, activeIdx);
290
+
291
+ // Γ‰tape 0 : Annuler TOUTES les animations en cours avant de commencer
292
+ // Cela Γ©vite les conflits si l'utilisateur scroll rapidement
293
+ items.forEach((li) => {
294
+ const sub = li.querySelector(":scope > ul");
295
+ if (sub) cancelAnimation(sub);
296
+ });
297
+
298
+ // Γ‰tape 1 : Identifier TOUS les Γ©lΓ©ments qui vont changer d'Γ©tat
299
+ const allChanges = [];
300
+ items.forEach((li) => {
301
+ const sub = li.querySelector(":scope > ul");
302
+ if (!sub) return;
303
+
304
+ const idx = Number(li.getAttribute("data-heading-idx") || "-1");
305
+
306
+ // Un Γ©lΓ©ment doit Γͺtre expanded SI il contient (directement ou indirectement) le heading actif
307
+ // Donc soit il est dans newActiveAncestors, soit un de ses descendants l'est
308
+ let shouldBeExpanded = false;
309
+
310
+ // VΓ©rifier si cet Γ©lΓ©ment ou un de ses descendants est dans le chemin actif
311
+ const allDescendants = li.querySelectorAll("li[data-heading-idx]");
312
+ const allRelatedIndices = [
313
+ idx,
314
+ ...Array.from(allDescendants).map((d) =>
315
+ Number(d.getAttribute("data-heading-idx") || "-1"),
316
+ ),
317
+ ];
318
+
319
+ // Si au moins un de ces indices est dans newActiveAncestors, garder ouvert
320
+ shouldBeExpanded = allRelatedIndices.some((i) =>
321
+ newActiveAncestors.has(i),
322
+ );
323
+
324
+ const isCurrentlyCollapsed = li.classList.contains("collapsed");
325
+
326
+ const isChanging =
327
+ (shouldBeExpanded && isCurrentlyCollapsed) ||
328
+ (!shouldBeExpanded && !isCurrentlyCollapsed);
329
+
330
+ if (isChanging) {
331
+ allChanges.push({ li, sub, shouldBeExpanded, idx });
332
+ }
333
+ });
334
+
335
+ // Γ‰tape 2 : Parmi tous les changements, trouver ceux qui sont des "top-level"
336
+ // (= n'ont PAS d'ancΓͺtre qui change aussi)
337
+ const topLevelChanges = [];
338
+ const descendantChanges = [];
339
+
340
+ allChanges.forEach((change) => {
341
+ let hasAncestorChanging = false;
342
+
343
+ // Remonter l'arbre pour voir si un ancΓͺtre change aussi
344
+ let currentLi = change.li;
345
+ while (currentLi) {
346
+ const parentLi = currentLi.parentElement?.closest(
347
+ "li[data-heading-idx]",
348
+ );
349
+ if (!parentLi) break;
350
+
351
+ const parentIdx = Number(
352
+ parentLi.getAttribute("data-heading-idx") || "-1",
353
+ );
354
+
355
+ // VΓ©rifier si ce parent est dans la liste des changements
356
+ const parentIsChanging = allChanges.some(
357
+ (c) => c.idx === parentIdx,
358
+ );
359
+ if (parentIsChanging) {
360
+ hasAncestorChanging = true;
361
+ break;
362
+ }
363
+
364
+ currentLi = parentLi;
365
+ }
366
+
367
+ if (hasAncestorChanging) {
368
+ descendantChanges.push(change);
369
+ } else {
370
+ topLevelChanges.push(change);
371
+ }
372
+ });
373
+
374
+ // Γ‰tape 3 : Appliquer TOUS les descendants instantanΓ©ment (sans animation)
375
+ // Ceci doit Γͺtre fait AVANT toute animation pour que les hauteurs soient correctes
376
+ if (descendantChanges.length > 0) {
377
+ descendantChanges.forEach(({ li, sub, shouldBeExpanded }) => {
378
+ const oldTransition = sub.style.transition;
379
+ sub.style.transition = "none";
380
+
381
+ if (shouldBeExpanded) {
382
+ li.classList.remove("collapsed");
383
+ sub.style.height = "auto";
384
+ } else {
385
+ li.classList.add("collapsed");
386
+ sub.style.height = "0px";
387
+ }
388
+
389
+ // Forcer un reflow immΓ©diat pour cet Γ©lΓ©ment
390
+ void sub.offsetHeight;
391
+ sub.style.transition = oldTransition || "";
392
+ });
393
+
394
+ // Forcer un reflow global pour que TOUS les changements soient appliquΓ©s
395
+ void document.body.offsetHeight;
396
+
397
+ // IMPORTANT : Attendre un frame pour que le navigateur ait fini tous les calculs
398
+ // avant de mesurer les hauteurs des parents
399
+ }
400
+
401
+ // Γ‰tape 4 : Animer SEULEMENT les top-level avec requestAnimationFrame
402
+ // Les descendants sont dΓ©jΓ  dans leur Γ©tat final, donc la hauteur du parent sera correcte
403
+ if (topLevelChanges.length > 0) {
404
+ // Double requestAnimationFrame pour Γͺtre sΓ»r que le DOM est stabilisΓ©
405
+ requestAnimationFrame(() => {
406
+ requestAnimationFrame(() => {
407
+ topLevelChanges.forEach(({ li, sub, shouldBeExpanded }) => {
408
+ if (shouldBeExpanded) {
409
+ li.classList.remove("collapsed");
410
+
411
+ // CRITIQUE : Avant de mesurer, mettre ABSOLUMENT TOUS les sous-Γ©lΓ©ments
412
+ // dans leur état final (expanded OU collapsed) de manière synchrone
413
+ const allInnerItems = sub.querySelectorAll(
414
+ "li[data-heading-idx]",
415
+ );
416
+
417
+ // D'abord, dΓ©sactiver toutes les transitions
418
+ allInnerItems.forEach((innerLi) => {
419
+ const innerSub = innerLi.querySelector(":scope > ul");
420
+ if (innerSub) {
421
+ innerSub.style.transition = "none";
422
+ }
423
+ });
424
+
425
+ // Ensuite, mettre chaque Γ©lΓ©ment dans son Γ©tat final
426
+ allInnerItems.forEach((innerLi) => {
427
+ const innerIdx = Number(
428
+ innerLi.getAttribute("data-heading-idx") || "-1",
429
+ );
430
+ const innerSub = innerLi.querySelector(":scope > ul");
431
+ if (innerSub) {
432
+ if (newActiveAncestors.has(innerIdx)) {
433
+ // Cet Γ©lΓ©ment devrait Γͺtre expanded
434
+ innerLi.classList.remove("collapsed");
435
+ innerSub.style.height = "auto";
436
+ } else {
437
+ // Cet Γ©lΓ©ment devrait Γͺtre collapsed
438
+ innerLi.classList.add("collapsed");
439
+ innerSub.style.height = "0px";
440
+ }
441
+ }
442
+ });
443
+
444
+ // Forcer un reflow global pour que TOUT soit calculΓ©
445
+ void sub.offsetHeight;
446
+
447
+ // RΓ©activer les transitions
448
+ allInnerItems.forEach((innerLi) => {
449
+ const innerSub = innerLi.querySelector(":scope > ul");
450
+ if (innerSub) {
451
+ innerSub.style.transition = "";
452
+ }
453
+ });
454
+
455
+ // Maintenant on peut mesurer avec confiance : tous les Γ©lΓ©ments
456
+ // sont dans leur Γ©tat final dΓ©finitif
457
+ const target = measure(sub);
458
+ animateTo(sub, target);
459
+ } else {
460
+ li.classList.add("collapsed");
461
+ animateTo(sub, 0);
462
+ }
463
+ });
464
+ });
465
+ });
466
+ }
467
+
468
+ prevActiveElements = newActiveAncestors;
469
+ };
470
+
471
+ update(sideItems);
472
+ update(mobileItems);
473
+ setNavCollapsible();
474
+ prevActiveIdx = activeIdx;
475
+ };
476
+
477
+ // When switching between desktop/mobile, refresh autoCollapse and expand all on mobile
478
+ const expandAll = () => {
479
+ const { sideItems, mobileItems } = getAllItemsWithChildren();
480
+ const expand = (items) =>
481
+ items.forEach((li) => {
482
+ li.classList.remove("collapsed");
483
+ const sub = li.querySelector(":scope > ul");
484
+ if (sub) sub.style.height = "auto";
485
+ });
486
+ expand(sideItems);
487
+ expand(mobileItems);
488
+ };
489
+
490
+ const onMqChange = () => {
491
+ autoCollapse = attrEnabled && !mq.matches;
492
+ if (!autoCollapse) {
493
+ expandAll();
494
+ } else {
495
+ setCollapsedState(prevActiveIdx);
496
+ }
497
+ };
498
+ if (mq.addEventListener) mq.addEventListener("change", onMqChange);
499
+ else if (mq.addListener) mq.addListener(onMqChange);
500
+
501
+ // Debounce pour traiter la dernière mise à jour après que le scroll se stabilise
502
+ let scrollDebounceTimer = null;
503
+ let lastRequestedIdx = -1;
504
+ let isProcessing = false;
505
+
506
+ // Fonction pour mettre Γ  jour l'URL avec l'ancre actuelle
507
+ const updateURL = (headingId) => {
508
+ if (!headingId) return;
509
+
510
+ const newUrl = `${window.location.pathname}${window.location.search}#${headingId}`;
511
+
512
+ // Mettre Γ  jour l'URL sans recharger la page
513
+ if (window.location.href !== newUrl) {
514
+ history.pushState(null, null, newUrl);
515
+
516
+ // Essayer diffΓ©rentes mΓ©thodes pour communiquer avec la fenΓͺtre parente
517
+ if (window.parent !== window) {
518
+ try {
519
+ // MΓ©thode 1: Essayer de modifier directement l'URL du parent (si autorisΓ©)
520
+ try {
521
+ window.parent.location.hash = headingId;
522
+ console.debug("Successfully updated parent URL directly");
523
+ return;
524
+ } catch (e) {
525
+ console.debug("Direct parent URL update blocked:", e.message);
526
+ }
527
+
528
+ // MΓ©thode 2: Essayer avec window.top (fenΓͺtre racine)
529
+ try {
530
+ if (window.top !== window && window.top !== window.parent) {
531
+ window.top.location.hash = headingId;
532
+ console.debug("Successfully updated top window URL directly");
533
+ return;
534
+ }
535
+ } catch (e) {
536
+ console.debug("Direct top window URL update blocked:", e.message);
537
+ }
538
+
539
+ // MΓ©thode 3: Utiliser postMessage avec diffΓ©rents formats vers parent
540
+ const messages = [
541
+ {
542
+ type: "urlChange",
543
+ url: newUrl,
544
+ hash: headingId,
545
+ },
546
+ {
547
+ type: "anchorChange",
548
+ anchorId: headingId,
549
+ url: newUrl,
550
+ },
551
+ {
552
+ type: "HF_SPACE_URL_UPDATE",
553
+ hash: headingId,
554
+ url: newUrl,
555
+ },
556
+ // Format officiel Hugging Face
557
+ {
558
+ queryString: "",
559
+ hash: headingId,
560
+ },
561
+ ];
562
+
563
+ messages.forEach((msg) => {
564
+ try {
565
+ window.parent.postMessage(msg, "*");
566
+ } catch (e) {
567
+ console.debug("PostMessage to parent failed:", e.message);
568
+ }
569
+ });
570
+
571
+ // MΓ©thode 4: Essayer avec window.top
572
+ try {
573
+ if (window.top !== window) {
574
+ messages.forEach((msg) => {
575
+ try {
576
+ window.top.postMessage(msg, "*");
577
+ } catch (e) {
578
+ console.debug("PostMessage to top failed:", e.message);
579
+ }
580
+ });
581
+ }
582
+ } catch (e) {
583
+ console.debug("PostMessage to top window failed:", e.message);
584
+ }
585
+
586
+ // MΓ©thode 5: Essayer avec l'origine spΓ©cifique Hugging Face
587
+ try {
588
+ window.parent.postMessage(
589
+ {
590
+ queryString: "",
591
+ hash: headingId,
592
+ },
593
+ "https://huggingface.co",
594
+ );
595
+ console.debug("Sent HF official format message");
596
+ } catch (e) {
597
+ console.debug("HF official format failed:", e.message);
598
+ }
599
+ } catch (e) {
600
+ console.debug(
601
+ "All parent communication methods failed:",
602
+ e.message,
603
+ );
604
+ }
605
+ }
606
+ }
607
+ };
608
+
609
+ const onScroll = () => {
610
+ // active link highlight
611
+ let activeIdx = -1;
612
+ let activeHeadingId = null;
613
+
614
+ for (let i = headingsArr.length - 1; i >= 0; i--) {
615
+ const top = headingsArr[i].getBoundingClientRect().top;
616
+ if (top - 60 <= 0) {
617
+ links.forEach((l) => l.classList.remove("active"));
618
+ const id = "#" + headingsArr[i].id;
619
+ const actives = Array.from(links).filter(
620
+ (l) => l.getAttribute("href") === id,
621
+ );
622
+ actives.forEach((a) => a.classList.add("active"));
623
+ // Utiliser l'index du heading actif (n'importe quel niveau)
624
+ activeIdx = i;
625
+ activeHeadingId = headingsArr[i].id;
626
+ break;
627
+ }
628
+ }
629
+
630
+ // Track active heading (without updating URL)
631
+ if (activeHeadingId) {
632
+ prevActiveHeadingId = activeHeadingId;
633
+ }
634
+
635
+ if (activeIdx === prevActiveIdx) return;
636
+
637
+ // Sauvegarder la dernière demande
638
+ lastRequestedIdx = activeIdx;
639
+
640
+ // Si on est en train de traiter, ne rien faire (on traitera la dernière demande après)
641
+ if (isProcessing) return;
642
+
643
+ // Debounce : attendre un peu que le scroll se stabilise
644
+ clearTimeout(scrollDebounceTimer);
645
+ scrollDebounceTimer = setTimeout(() => {
646
+ // Traiter la dernière demande
647
+ if (lastRequestedIdx !== prevActiveIdx) {
648
+ isProcessing = true;
649
+ setCollapsedState(lastRequestedIdx);
650
+ // Le processing flag sera réinitialisé après les animations
651
+ setTimeout(() => {
652
+ isProcessing = false;
653
+ // Si une nouvelle demande est arrivΓ©e pendant qu'on traitait, la traiter maintenant
654
+ if (lastRequestedIdx !== prevActiveIdx) {
655
+ onScroll();
656
+ }
657
+ }, 250); // Attendre que les animations soient lancΓ©es
658
+ }
659
+ }, 100); // Debounce de 100ms
660
+ };
661
+
662
+ // If auto-collapse, collapse immediately (expand first section) before any scroll
663
+ if (autoCollapse) setCollapsedState(0);
664
+
665
+ window.addEventListener("scroll", onScroll);
666
+
667
+ // GΓ©rer la navigation par ancres au chargement de la page
668
+ const handleInitialNavigation = () => {
669
+ const hash = window.location.hash;
670
+ if (hash) {
671
+ const targetElement = document.querySelector(hash);
672
+ if (targetElement) {
673
+ // Attendre que le DOM soit prΓͺt puis faire dΓ©filer vers l'Γ©lΓ©ment
674
+ setTimeout(() => {
675
+ targetElement.scrollIntoView({ block: "start" });
676
+ // Mettre à jour l'URL après le scroll
677
+ setTimeout(() => {
678
+ updateURL(hash.substring(1)); // Enlever le # du hash
679
+ }, 100);
680
+ }, 100);
681
+ }
682
+ } else {
683
+ // No hash on load β€” just highlight the first section in the TOC
684
+ // without modifying the URL (prevents unwanted auto-scroll on reload)
685
+ }
686
+ };
687
+
688
+ // Initialize state
689
+ onScroll();
690
+
691
+ // GΓ©rer la navigation initiale
692
+ handleInitialNavigation();
693
+
694
+ // GΓ©rer les Γ©vΓ©nements de navigation du navigateur (boutons prΓ©cΓ©dent/suivant)
695
+ window.addEventListener("popstate", (event) => {
696
+ const hash = window.location.hash;
697
+ if (hash) {
698
+ const targetElement = document.querySelector(hash);
699
+ if (targetElement) {
700
+ targetElement.scrollIntoView({ block: "start" });
701
+ }
702
+ } else {
703
+ // Si pas d'ancre, aller au dΓ©but de la page
704
+ window.scrollTo({ top: 0 });
705
+ }
706
+ });
707
+
708
+ // Close mobile accordion when a link inside it is clicked
709
+ if (holderMobile) {
710
+ const details = holderMobile.closest("details");
711
+ holderMobile.addEventListener("click", (ev) => {
712
+ const target = ev.target;
713
+ const anchor =
714
+ target && "closest" in target ? target.closest("a") : null;
715
+ if (anchor instanceof HTMLAnchorElement && details && details.open) {
716
+ details.open = false;
717
+ }
718
+ });
719
+ }
720
+ };
721
+
722
+ if (document.readyState === "loading") {
723
+ document.addEventListener("DOMContentLoaded", buildTOC, { once: true });
724
+ } else {
725
+ buildTOC();
726
+ }
727
+ </script>
728
+
729
+ <style is:global>
730
+ /* Sticky aside */
731
+ .table-of-contents {
732
+ position: sticky;
733
+ top: 32px;
734
+ margin-top: 12px;
735
+ }
736
+
737
+ .table-of-contents nav {
738
+ border-left: 1px solid var(--border-color);
739
+ padding-left: 16px;
740
+ font-size: 13px;
741
+ }
742
+
743
+ .table-of-contents .title {
744
+ font-weight: 600;
745
+ font-size: 14px;
746
+ margin-bottom: 8px;
747
+ }
748
+
749
+ /* Look & feel */
750
+ .table-of-contents nav ul {
751
+ margin: 0 0 6px;
752
+ padding-left: 1em;
753
+ }
754
+
755
+ .table-of-contents nav li {
756
+ list-style: none;
757
+ margin: 0.25em 0;
758
+ }
759
+
760
+ .table-of-contents nav a,
761
+ .table-of-contents nav a:link,
762
+ .table-of-contents nav a:visited {
763
+ color: var(--text-color);
764
+ text-decoration: none;
765
+ border-bottom: none;
766
+ }
767
+
768
+ .table-of-contents nav > ul > li > a {
769
+ font-weight: 700;
770
+ }
771
+
772
+ .table-of-contents nav a:hover {
773
+ text-decoration: underline solid var(--muted-color);
774
+ }
775
+
776
+ .table-of-contents nav a.active {
777
+ text-decoration: underline;
778
+ }
779
+
780
+ /* Mobile accordion */
781
+ .table-of-contents-mobile {
782
+ display: none;
783
+ margin: 8px 0 16px;
784
+ }
785
+
786
+ .table-of-contents-mobile > summary {
787
+ cursor: pointer;
788
+ list-style: none;
789
+ padding: var(--spacing-3) var(--spacing-4);
790
+ border: 1px solid var(--border-color);
791
+ border-radius: 8px;
792
+ color: var(--text-color);
793
+ font-weight: 600;
794
+ position: relative;
795
+ }
796
+
797
+ .table-of-contents-mobile[open] > summary {
798
+ border-bottom-left-radius: 0;
799
+ border-bottom-right-radius: 0;
800
+ }
801
+
802
+ /* Disclosure arrow for mobile summary */
803
+ .table-of-contents-mobile > summary::after {
804
+ content: "";
805
+ position: absolute;
806
+ right: var(--spacing-4);
807
+ top: 50%;
808
+ width: 8px;
809
+ height: 8px;
810
+ border-right: 2px solid currentColor;
811
+ border-bottom: 2px solid currentColor;
812
+ transform: translateY(-70%) rotate(45deg);
813
+ transition: transform 150ms ease;
814
+ opacity: 0.7;
815
+ }
816
+
817
+ .table-of-contents-mobile[open] > summary::after {
818
+ transform: translateY(-30%) rotate(-135deg);
819
+ }
820
+
821
+ .table-of-contents-mobile nav {
822
+ border-left: none;
823
+ padding: 10px 12px;
824
+ font-size: 14px;
825
+ border: 1px solid var(--border-color);
826
+ border-top: none;
827
+ border-bottom-left-radius: 8px;
828
+ border-bottom-right-radius: 8px;
829
+ }
830
+
831
+ .table-of-contents-mobile nav ul {
832
+ margin: 0 0 6px;
833
+ padding-left: 1em;
834
+ }
835
+
836
+ .table-of-contents-mobile nav li {
837
+ list-style: none;
838
+ margin: 0.25em 0;
839
+ }
840
+
841
+ .table-of-contents-mobile nav a,
842
+ .table-of-contents-mobile nav a:link,
843
+ .table-of-contents-mobile nav a:visited {
844
+ color: var(--text-color);
845
+ text-decoration: none;
846
+ border-bottom: none;
847
+ }
848
+
849
+ .table-of-contents-mobile nav > ul > li > a {
850
+ font-weight: 700;
851
+ }
852
+
853
+ .table-of-contents-mobile nav a:hover {
854
+ text-decoration: underline solid var(--muted-color);
855
+ }
856
+
857
+ .table-of-contents-mobile nav a.active {
858
+ text-decoration: underline;
859
+ }
860
+ </style>
app/src/components/ThemeToggle.astro ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <button id="theme-toggle" aria-label="Toggle color theme">
2
+ <svg
3
+ class="icon light"
4
+ width="20"
5
+ height="20"
6
+ viewBox="0 0 24 24"
7
+ aria-hidden="true"
8
+ focusable="false"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ stroke-width="2"
12
+ stroke-linecap="round"
13
+ stroke-linejoin="round"
14
+ >
15
+ <circle cx="12" cy="12" r="5"></circle>
16
+ <line x1="12" y1="1" x2="12" y2="4"></line>
17
+ <line x1="12" y1="20" x2="12" y2="23"></line>
18
+ <line x1="1" y1="12" x2="4" y2="12"></line>
19
+ <line x1="20" y1="12" x2="23" y2="12"></line>
20
+ <line x1="4.22" y1="4.22" x2="6.34" y2="6.34"></line>
21
+ <line x1="17.66" y1="17.66" x2="19.78" y2="19.78"></line>
22
+ <line x1="4.22" y1="19.78" x2="6.34" y2="17.66"></line>
23
+ <line x1="17.66" y1="6.34" x2="19.78" y2="4.22"></line>
24
+ </svg>
25
+ <svg
26
+ class="icon dark"
27
+ width="20"
28
+ height="20"
29
+ viewBox="0 0 24 24"
30
+ aria-hidden="true"
31
+ focusable="false"
32
+ fill="none"
33
+ stroke="currentColor"
34
+ stroke-width="2"
35
+ stroke-linecap="round"
36
+ stroke-linejoin="round"
37
+ >
38
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
39
+ </svg>
40
+ <script>
41
+ const btn = document.getElementById("theme-toggle");
42
+ const media =
43
+ window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
44
+ const prefersDark = media && media.matches;
45
+ const saved = localStorage.getItem("theme"); // 'light' | 'dark' | null
46
+
47
+ const apply = (mode) => {
48
+ document.documentElement.dataset.theme = mode;
49
+ };
50
+
51
+ // Initial mode: default to light, then use localStorage, else system
52
+ apply(saved || (prefersDark ? "dark" : "light"));
53
+
54
+ // If user hasn't chosen manually, follow system changes
55
+ if (!saved && media) {
56
+ const syncWithSystem = (e) => apply(e.matches ? "dark" : "light");
57
+ if (media.addEventListener)
58
+ media.addEventListener("change", syncWithSystem);
59
+ else if (media.addListener) media.addListener(syncWithSystem);
60
+ }
61
+
62
+ const toggleTheme = () => {
63
+ const next =
64
+ document.documentElement.dataset.theme === "dark" ? "light" : "dark";
65
+ localStorage.setItem("theme", next);
66
+ apply(next);
67
+ };
68
+
69
+ if (btn) {
70
+ btn.addEventListener("click", toggleTheme);
71
+ }
72
+
73
+ // Ajouter le raccourci clavier pour la touche D
74
+ document.addEventListener("keydown", (e) => {
75
+ // VΓ©rifier que la touche D est pressΓ©e et qu'on n'est pas dans un input/textarea
76
+ if (e.key === "d" || e.key === "D") {
77
+ const activeElement = document.activeElement;
78
+ const isInput =
79
+ activeElement &&
80
+ (activeElement.tagName === "INPUT" ||
81
+ activeElement.tagName === "TEXTAREA" ||
82
+ (activeElement as HTMLElement).contentEditable === "true");
83
+
84
+ if (!isInput) {
85
+ e.preventDefault();
86
+ toggleTheme();
87
+ }
88
+ }
89
+ });
90
+ </script>
91
+ </button>
92
+
93
+ <style>
94
+ #theme-toggle {
95
+ display: inline-flex;
96
+ align-items: center;
97
+ gap: 8px;
98
+ border: none;
99
+ background: transparent;
100
+ padding: 6px 10px;
101
+ border-radius: 8px;
102
+ cursor: pointer;
103
+ color: var(--text-color) !important;
104
+ }
105
+ #theme-toggle .icon.dark {
106
+ display: none;
107
+ }
108
+ [data-theme="dark"] #theme-toggle .icon.light {
109
+ display: none;
110
+ }
111
+ [data-theme="dark"] #theme-toggle .icon.dark {
112
+ display: inline;
113
+ }
114
+ #theme-toggle .icon {
115
+ filter: none !important;
116
+ }
117
+ </style>
app/src/components/Wide.astro ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ const { class: className, ...props } = Astro.props;
3
+ const wrapperClass = ["wide", className].filter(Boolean).join(" ");
4
+ ---
5
+ <div class={wrapperClass} {...props}>
6
+ <slot />
7
+ </div>
8
+
9
+
app/src/content/article.mdx ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ seoThumbImage: 'https://seongland.com/article/crl/og.png'
3
+ title: 'Control Reinforcement Learning'
4
+ subtitle: >-
5
+ Interpretable Token-Level Steering of LLMs via SAE Features
6
+ description: 'CRL: Interpretable Token-Level Steering of LLMs via Sparse Autoencoder Features'
7
+ authors:
8
+ - name: Seonglae Cho
9
+ affiliations:
10
+ - 1
11
+ - 2
12
+ - name: Zekun Wu
13
+ affiliations:
14
+ - 1
15
+ - 2
16
+ - name: Adriano Koshiyama
17
+ affiliations:
18
+ - 1
19
+ - 2
20
+ affiliations:
21
+ - name: Holistic AI
22
+ url: 'https://holisticai.com'
23
+ - name: University College London
24
+ url: 'https://ucl.ac.uk'
25
+ published: 'Feb. 13, 2026'
26
+ licence: >
27
+ Diagrams and text are licensed under <a
28
+ href="https://creativecommons.org/licenses/by/4.0/" target="_blank"
29
+ rel="noopener noreferrer">CC‑BY 4.0</a>.
30
+ doi: '10.48550/arXiv.2602.10437'
31
+ arxiv: '2602.10437'
32
+ tags:
33
+ - mechanistic interpretability
34
+ - sparse autoencoders
35
+ - reinforcement learning
36
+ - language models
37
+ - feature attribution
38
+
39
+ tableOfContentsAutoCollapse: true
40
+ ---
41
+
42
+ import Sidenote from '../components/Sidenote.astro';
43
+ import HtmlEmbed from '../components/HtmlEmbed.astro';
44
+ import Note from '../components/Note.astro';
45
+ import Wide from '../components/Wide.astro';
46
+ import FullWidth from '../components/FullWidth.astro';
47
+ import Quote from '../components/Quote.astro';
48
+ import Reference from '../components/Reference.astro';
49
+
50
+ ## Abstract
51
+
52
+ Watching a feature light up tells you it was present. It does not tell you it did anything. [Sparse autoencoders](https://texonom.com/1fa3c713076d4e34aa43f7c1dce62a94) (SAEs) decompose language model activations into interpretable features [@bricken2023monosemanticity; @huben2024sparse], and almost everything built on them reads off *which* features activate, not which ones actually **change model outputs** when amplified. We introduce **Control [Reinforcement Learning](https://texonom.com/3e6e1e5b806941b6b586ce2c3d1bbd02) (CRL)**, which trains a policy to select SAE features for steering at each token, producing interpretable intervention logs.
53
+
54
+ **Adaptive Feature Masking** encourages diverse feature discovery while preserving single-feature interpretability. The framework yields new analysis capabilities: **branch point tracking** locates tokens where feature choice determines output correctness; **critic trajectory analysis** separates policy limitations from value estimation errors; and **layer-wise comparison** reveals syntactic features in early layers and semantic features in later layers [@tenney2019bert].
55
+
56
+ On [Gemma 2](https://texonom.com/8aa07c8eba2c4d4ba61213b9a4f711c5) 2B [@gemmateam2024gemma2improvingopen] across [MMLU](https://texonom.com/d2a91b61ee71491d9e6e284a5bdcc709) [@hendrycks2021measuring], [BBQ](https://texonom.com/14bc3c96247d81038bb3c4aa92f78a0d) [@parrish-etal-2022-bbq], GSM8K [@cobbe2021trainingverifierssolvemath], HarmBench, and [XSTest](https://texonom.com/237c3c96247d8090a166c4cbee0dc287), CRL achieves consistent improvements while providing per-token intervention logs. These results establish learned feature steering as a [mechanistic interpretability](https://texonom.com/e40305fd878c46ca85d99ea93ee9a2ff) tool that complements static feature analysis with dynamic intervention probes.
57
+
58
+ <Wide>
59
+ <HtmlEmbed src="steering-demo.html" title="Token-Level Steering Simulation" desc="Figure 2: Watch how different SAE features steer token generation along divergent paths. The same prompt context leads to correct or incorrect answers depending on which feature is amplified. Feature IDs link to Neuronpedia for detailed activation analysis." />
60
+ </Wide>
61
+
62
+ ## The Problem: Static Feature Analysis Falls Short
63
+
64
+ Sparse autoencoders have emerged as a powerful tool for decomposing the internal representations of language models. By training an encoder-decoder pair with a sparsity penalty, SAEs recover interpretable features from the [residual stream](https://texonom.com/beb7f1e3c9334362b518f0350300780a) β€” components that correspond to concepts like "mathematical reasoning" or "safety refusal patterns."
65
+
66
+ <Sidenote>
67
+ SAEs learn: $\mathcal{L} = \|\mathbf{x} - \hat{\mathbf{x}}\|^2 + \lambda \|\mathbf{z}\|_1$ where $\mathbf{z}$ is the sparse feature activation vector.
68
+ </Sidenote>
69
+
70
+ But there's a fundamental limitation: **static analysis tells you which features are present, not which ones matter.** Knowing that [Feature 7708](https://neuronpedia.org/gemma-2-2b/20-gemmascope-res-16k/7708) ("mathematical operations") activates on a math problem doesn't tell you whether *amplifying* that feature would improve the model's answer, make it worse, or have no effect at all.
71
+
72
+ This gap between activation analysis and causal intervention is where CRL enters. Instead of cataloging which features fire, we ask: **which features, when amplified, actually change the model's output?**
73
+
74
+ <Note variant="info">
75
+ **The key distinction:** Traditional SAE analysis is like reading a map β€” you can see the terrain. CRL is like walking through it β€” you discover which paths actually lead somewhere different.
76
+ </Note>
77
+
78
+ Current approaches to SAE-based control face limitations:
79
+ - **Contrastive methods** require carefully curated positive/negative datasets for each feature
80
+ - **[Activation patching](https://texonom.com/11fc3c96247d80d683e6c67f149c992d)** requires extensive storage and computation for exhaustive feature probing
81
+ - **Manual coefficient tuning** doesn't generalize across layers or tasks
82
+
83
+ CRL addresses these by framing feature selection as a *learned* decision at every token, using reinforcement learning to discover which features are worth amplifying.
84
+
85
+
86
+ ## How CRL Works
87
+
88
+ <Wide>
89
+ <HtmlEmbed src="crl-flowchart-animated.html" title="CRL System Overview" desc="Figure 3: Interactive CRL system diagram. Hover over components to see their role. The policy network observes the residual stream and selects SAE features for steering, while the critic estimates expected outcomes for PPO training." />
90
+ </Wide>
91
+
92
+ ### Formulating Steering as an MDP
93
+
94
+ CRL treats the control of transformer representations as a **[Markov Decision Process](https://texonom.com/d01c5c23d56648ce843727514008e25c)** over SAE features:
95
+
96
+ <div class="crl-equation">
97
+
98
+ $$\tilde{\mathbf{x}}_t = \mathbf{x}_t + c \cdot \mathbf{a}_t \mathbf{W}_{dec}$$
99
+
100
+ </div>
101
+
102
+ where $\mathbf{x}_t$ is the residual stream activation at token $t$, $\mathbf{a}_t$ is the binary feature selection vector, $c$ is the steering coefficient, and $\mathbf{W}_{dec}$ is the SAE decoder matrix.
103
+
104
+ The MDP components are:
105
+
106
+ - **State** $\mathbf{s} = \mathbf{x}$: the residual stream activation at the intervention layer
107
+ - **Action** $\mathbf{a} \in \{0,1\}^{d_{dict}}$: a sparse binary vector selecting SAE features (typically $k=1$)
108
+ - **Transition**: deterministic, governed by the transformer's forward pass with the steering intervention
109
+ - **Reward**: task-specific binary signal (correct/incorrect output)
110
+
111
+ <Sidenote>
112
+ Using $k=1$ (single feature selection) is critical for interpretability β€” each intervention is attributable to exactly one SAE feature.
113
+ </Sidenote>
114
+
115
+ ### The Policy and Critic Networks
116
+
117
+ <Wide>
118
+ <HtmlEmbed src="policy-critic-diagram.html" title="Policy & Critic Architecture" desc="Figure: The policy observes the residual stream per token and selects SAE features for steering. The critic estimates state values for advantage computation from verifiable rewards." />
119
+ </Wide>
120
+
121
+ The **policy network** $\pi_\theta$ is a 2-layer MLP with Tanh activation that maps residual stream activations to feature selection logits:
122
+
123
+ $$\boldsymbol{\mu} = \pi_\theta(\mathbf{s}), \quad \mathbf{a} = \text{TopK}(\boldsymbol{\mu}, k)$$
124
+
125
+ During training, actions are sampled from a categorical distribution over softmax probabilities for unbiased [policy gradients](https://texonom.com/331be391bdf44796a94ac9d6ddfe6c01). During evaluation, we use deterministic argmax for reproducibility.
126
+
127
+ The **critic network** $V_\phi$ estimates the state [value function](https://texonom.com/d9d3467fea994443b2336fd7958dc00b), enabling advantage estimation for [PPO](https://texonom.com/87ce8ebe81f84ac1a7617b1f6def9e26) [@schulman2017proximal] training. Crucially, the critic also serves as an *analysis tool* β€” its learned value estimates reveal where the policy succeeds and fails.
128
+
129
+ <Note variant="info">
130
+ **Why PPO over [DPO](https://texonom.com/d4099259ad6d443e8f9c2c9d1b45d6db)/[GRPO](https://texonom.com/187c3c96247d80dfb88bceaf4abad43c) [@shao2024deepseekmathpushinglimitsmathematical]?** PPO provides an explicit value function that we analyze as a diagnostic tool. The critic trajectories reveal bottleneck patterns that would be invisible with preference-based methods.
131
+ </Note>
132
+
133
+ ### Partial Observability
134
+
135
+ CRL-Token operates under partial observability: the policy sees only the residual stream at layer $\ell$, without access to the tokens that will be sampled. We mitigate this by using temperature-0 sampling, making the generation deterministic given the residual stream state.
136
+
137
+
138
+ ## Adaptive Feature Masking
139
+
140
+ A naive policy would collapse to repeatedly selecting the same high-reward feature. **Adaptive Feature Masking (AFM)** prevents this by dynamically managing which features are available for selection.
141
+
142
+ <div class="crl-equation">
143
+
144
+ $$\text{mask}_i^{(t+1)} = \text{mask}_i^{(t)} \lor \text{active}_i^{(t)}$$
145
+
146
+ </div>
147
+
148
+ The mask starts small and **progressively expands** as the model generates tokens β€” features that naturally activate during generation become available for future policy selections:
149
+
150
+ 1. Policy computes logits and masks unavailable features: $\mathbf{a}_t = \text{TopK}(\boldsymbol{\mu} \odot \text{mask}^{(t)}, k)$
151
+ 2. Steering is applied to the residual stream
152
+ 3. Natural SAE activations at the current token expand the mask for the next step
153
+
154
+ This creates a virtuous cycle: early steering may activate new features that become candidates for later steering, encouraging diverse feature discovery while building on the model's own representations.
155
+
156
+ <Note variant="success">
157
+ AFM is particularly important for multi-token tasks like GSM8K, where the reasoning chain spans hundreds of tokens and different features become relevant at different stages.
158
+ </Note>
159
+
160
+
161
+ ## Branch Analysis: Where Steering Decides the Answer
162
+
163
+ This is where CRL's interpretability capabilities become most compelling. **Branch analysis** identifies tokens where the *choice of feature* determines whether the model produces a correct or incorrect answer.
164
+
165
+ ### How Branch Analysis Works
166
+
167
+ For each generation, we track:
168
+ 1. The **context** (shared prefix before the branch point)
169
+ 2. The **correct path** β€” the feature that leads to a correct answer
170
+ 3. The **incorrect path** β€” the feature that leads to a wrong answer
171
+
172
+ This provides direct causal evidence that specific features are responsible for output quality, not just correlated with it.
173
+
174
+ ### Layer-Specific Feature Semantics
175
+
176
+ Comparing features selected at Layer 10 vs Layer 20 reveals a striking pattern:
177
+
178
+ <div class="crl-comparison">
179
+ <div class="crl-comparison__side crl-comparison__side--correct">
180
+ <h4>Layer 10: Syntactic Features</h4>
181
+ <ul>
182
+ <li><a href="https://neuronpedia.org/gemma-2-2b/10-gemmascope-res-16k/16145"><span class="crl-layer-badge">L10/16145</span></a> "mathematical notation"</li>
183
+ <li><a href="https://neuronpedia.org/gemma-2-2b/10-gemmascope-res-16k/5423"><span class="crl-layer-badge">L10/5423</span></a> "pronoun references"</li>
184
+ <li><a href="https://neuronpedia.org/gemma-2-2b/10-gemmascope-res-16k/8428"><span class="crl-layer-badge">L10/8428</span></a> "clinical examinations" (surface similarity)</li>
185
+ <li><a href="https://neuronpedia.org/gemma-2-2b/10-gemmascope-res-16k/8775"><span class="crl-layer-badge">L10/8775</span></a> "legal contracts" (terminology match)</li>
186
+ </ul>
187
+ </div>
188
+ <div class="crl-comparison__side crl-comparison__side--incorrect">
189
+ <h4>Layer 20: Semantic Features</h4>
190
+ <ul>
191
+ <li><a href="https://neuronpedia.org/gemma-2-2b/10-gemmascope-res-16k/12607"><span class="crl-layer-badge">L10/12607</span></a> "mathematical proofs" (logical structure)</li>
192
+ <li><a href="https://neuronpedia.org/gemma-2-2b/20-gemmascope-res-16k/11163"><span class="crl-layer-badge">L20/11163</span></a> "assessments and strategic plans"</li>
193
+ <li><a href="https://neuronpedia.org/gemma-2-2b/20-gemmascope-res-16k/2878"><span class="crl-layer-badge">L20/2878</span></a> "code formatting" (structural patterns)</li>
194
+ <li><a href="https://neuronpedia.org/gemma-2-2b/20-gemmascope-res-16k/998"><span class="crl-layer-badge">L20/998</span></a> "needs identification" (requirement semantics)</li>
195
+ </ul>
196
+ </div>
197
+ </div>
198
+
199
+ Layer 10 features capture **concrete syntactic patterns** β€” surface-level notation, entity tracking, formatting. Layer 20 features encode **abstract semantics** β€” logical derivation structure, planning, and strategic reasoning.
200
+
201
+ <Note variant="danger">
202
+ **Feature labels have limited predictive value.** A feature labeled "emotional relationships" can solve speed calculations, while "mathematical notation" fails on some arithmetic. This indicates learned steering effects diverge from semantic interpretations derived from activation patterns alone.
203
+ </Note>
204
+
205
+ Out of 8 analyzed branch pairs, Layer 10 features succeed on **3 cases** (discrete counting, direct arithmetic), while Layer 20 features succeed on **5 cases** (multi-step accumulation, variable reasoning).
206
+
207
+
208
+ ## Empirical Validation
209
+
210
+ To confirm that the learned policy selects *meaningful* features β€” not arbitrary ones β€” we compare CRL against baselines on five benchmarks. The goal is not performance gain per se, but to validate that the policy has learned a non-trivial feature selection strategy worth analyzing.
211
+
212
+ <HtmlEmbed src="results-table.html" title="CRL Steering Results" desc="Table 1: Before vs. after CRL steering. Toggle between Gemma 2 2B and [LLaMA](https://texonom.com/f2b6721202d44d469add84d8a366809c) 3.1 8B to compare across tasks and architectures." />
213
+
214
+ ### Learned vs. Random Selection
215
+
216
+ <Wide>
217
+ <HtmlEmbed src="baseline-comparison.html" title="Steering Baseline Comparison" desc="Figure 5: Random feature selection can degrade performance (βˆ’1.81 on BBQ), confirming the policy learns non-trivial feature selection." />
218
+ </Wide>
219
+
220
+ | Method | BBQ Ambig | HarmBench |
221
+ |--------|-----------|-----------|
222
+ | No Steering (Base) | 60.17 | 41.46 |
223
+ | Random Feature | 58.36 | 45.35 |
224
+ | Random + AFM | 60.16 | 46.96 |
225
+ | Most-Active Feature | 59.94 | 48.03 |
226
+ | **CRL (Ours)** | **65.86** | **49.12** |
227
+
228
+ Random feature steering can actually *hurt* performance (βˆ’1.81 on BBQ Ambig), while selecting the most-active SAE feature barely helps. The learned policy discovers features whose *causal effects* differ from their activation magnitude β€” precisely the gap that static analysis cannot reveal.
229
+
230
+
231
+ ## Critic Trajectory Analysis
232
+
233
+ The learned critic (value function) reveals *where* in the generation process the model's confidence changes β€” and whether task limitations stem from the policy or the value estimator.
234
+
235
+ <Wide>
236
+ <HtmlEmbed src="critic-trajectories.html" title="Interactive Critic Trajectory Explorer" desc="Figure 7: Explore critic value trajectories across tasks. GSM8K shows multi-token generation (~1000 steps); HarmBench and XSTest show short-form generation (~30 steps). Toggle between tasks to compare." />
237
+ </Wide>
238
+
239
+ ### Key Observations
240
+
241
+ For multi-token generation (GSM8K), critic trajectories reveal the *temporal dynamics* of steering:
242
+
243
+ - **Correct samples** (green): Higher gradients with divergence around token 200–400
244
+ - **Incorrect samples** (red): Flat or declining values throughout
245
+ - **Corrected samples** (blue): Show value increases at the ~400-token point, indicating interventions become more effective in later reasoning stages
246
+ - **Misguided samples** (yellow): Initially high values that drop β€” the steering seemed promising but ultimately led astray
247
+
248
+
249
+ ## Feature Interpretability
250
+
251
+ CRL provides per-token intervention logs that reveal *which* features are selected and *when*. This enables a new form of mechanistic analysis.
252
+
253
+ ### Feature-Token Alignment
254
+
255
+ <Wide>
256
+ <HtmlEmbed src="token-highlights.html" title="Token-Level Feature Activation Highlights" desc="Figures 8-11: Interactive token-level visualization of SAE feature activations on GSM8K reasoning chains. Corrected examples show feature activations aligning with mathematical tokens; misguided examples show token-feature misalignment. Hover tokens for details; click feature pills to view on Neuronpedia." />
257
+ </Wide>
258
+
259
+ **Corrected examples** show clear semantic coherence: features activate on mathematical tokens, operators, and quantitative units -- demonstrating lexical generalization beyond surface forms.
260
+
261
+ ### When Features Fail
262
+
263
+ **Misguided examples** (select the "Misguided" tab above) reveal failure modes:
264
+ - Task-irrelevant features disrupting the reasoning chain
265
+ - Token-feature misalignment producing incoherent activations
266
+ - Feature interference causing the model to abandon its solution path
267
+
268
+ ### Feature Diversity
269
+
270
+ <HtmlEmbed src="feature-frequency.html" title="Feature Selection Frequency" desc="Figure 13: Feature selection frequency follows a rank-dependent decay, confirming the policy learns selective (not random) feature choices. Click feature IDs to view on Neuronpedia." />
271
+
272
+ Feature diversity varies significantly across tasks, measured by entropy of the feature selection distribution:
273
+
274
+ | Task | Feature Diversity | Interpretation |
275
+ |------|------------------|----------------|
276
+ | GSM8K | 6.695 | Complex reasoning needs diverse features |
277
+ | MMLU-Pro | 5.476 | High complexity, broad engagement |
278
+ | XSTest | 4.757 | Intermediate diversity |
279
+ | HarmBench | 2.938 | Focused refusal uses concentrated features |
280
+
281
+ Complex reasoning tasks require *broader* feature engagement, while focused behavioral tasks benefit from *concentrated* feature activation.
282
+
283
+
284
+ ## Layer-Wise Analysis
285
+
286
+ Different layers encode different types of information, and CRL's layer-wise training reveals this structure.
287
+
288
+ <Wide>
289
+ <HtmlEmbed src="layer-heatmap.html" title="Layer Γ— Coefficient Sensitivity" desc="Figure 14: Interactive heatmap showing accuracy across layer-coefficient combinations. Each task has a different optimal configuration. Hover cells for details." />
290
+ </Wide>
291
+
292
+ ### Residual Stream Norms
293
+
294
+ <HtmlEmbed src="norm-growth.html" title="Residual Stream Norm Growth" desc="Figure 15: Residual stream norms increase across layers, requiring larger steering coefficients in deeper layers. Hover to see exact values and coefficient ranges." />
295
+
296
+ Residual stream norms increase with depth, which directly impacts the optimal steering coefficient:
297
+ - **Early layers** (L5–L10): $c = 10\text{–}20$
298
+ - **Middle layers** (L15–L20): $c = 30\text{–}60$
299
+ - **Late layers** (L22–L25): $c = 60\text{–}100$
300
+
301
+ Large coefficients degrade early-layer performance, while late layers tolerate β€” and require β€” stronger interventions.
302
+
303
+
304
+ ## Discussion
305
+
306
+ ### What CRL Reveals as an Analysis Tool
307
+
308
+ CRL provides three diagnostic capabilities that complement static feature analysis:
309
+
310
+ **1. Branch tracking** locates critical decision points β€” tokens where the choice of a single SAE feature determines whether the output is correct or incorrect. This provides *causal* evidence for feature importance, going beyond correlational activation analysis.
311
+
312
+ **2. Critic trajectories** separate policy from value-estimation limitations. When the critic shows a bottleneck (MMLU), we know the value function is the limiting factor, not the policy's feature selection. When the critic shows clear discrimination (BBQ), the policy-critic coordination is effective.
313
+
314
+ **3. Layer comparison** reveals a syntactic-to-semantic gradient: early layers encode concrete patterns (notation, formatting), while later layers capture abstract structure (logical derivation, strategic planning).
315
+
316
+ ### Limitations
317
+
318
+ - **Layer selection is post-hoc:** optimal layers are identified via validation, not predicted a priori
319
+ - **Feature labels are unreliable:** SAE feature descriptions often diverge from their actual steering effects
320
+ - **Single-feature constraint:** selecting $k=1$ maximizes interpretability but may leave performance on the table
321
+ - **Computational cost:** while modest (8–9% overhead), CRL requires PPO training for each task-layer combination
322
+
323
+
324
+ ## Conclusion
325
+
326
+ Control Reinforcement Learning transforms SAE feature steering from a static analysis tool into a **dynamic, token-level intervention framework** with built-in interpretability. By training a policy to select features at each generation step, CRL produces mechanistic analysis tools:
327
+
328
+ - **Branch tracking** locates critical decision points where feature choice determines output correctness
329
+ - **Critic trajectories** separate policy limitations from value-estimation errors
330
+ - **Layer comparison** reveals syntactic-to-semantic feature gradients across network depth
331
+
332
+ Validated across knowledge, bias, reasoning, and safety benchmarks, CRL maintains single-feature interpretability throughout. The framework establishes learned feature steering as a practical mechanistic interpretability tool that complements static feature analysis with dynamic intervention probes.
app/src/content/assets/data/bbq-ambig-layer-sweep.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:99f3cada76e74b16d3d4fdc66f320359a7f2f5844bb2eb787343f5dff734bc5f
3
+ size 3136
app/src/content/assets/data/bbq-disambig-layer-sweep.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bbdcdb11ef584f5a95bc6213ac100391859a15ab938c7e25d4482b809663175b
3
+ size 2264
app/src/content/assets/data/mmlu-layer-sweep-constrained.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2173d7c14090dcf34446f00266a99e719112e4068500a6baf0af57ba09cad9ca
3
+ size 2880
app/src/content/assets/data/mmlu-layer-sweep.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cfc679c136321b7cda6490f334e8cf132bafac1a649a4f1698af018c13800ddf
3
+ size 7423
app/src/content/assets/image/flowchart.png ADDED

Git LFS Details

  • SHA256: af692a05baf908a971a388b758e19cad74005880d3319f581afbb64c6839a913
  • Pointer size: 131 Bytes
  • Size of remote file: 578 kB
app/src/content/bibliography.bib ADDED
@@ -0,0 +1,1739 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ % LLM
2
+ @misc{nikolaou2025languagemodelsinjectiveinvertible,
3
+ title={Language Models are Injective and Hence Invertible},
4
+ author={Giorgos Nikolaou and Tommaso Mencattini and Donato Crisostomi and Andrea Santilli and Yannis Panagakis and Emanuele RodolΓ },
5
+ year={2025},
6
+ eprint={2510.15511},
7
+ archivePrefix={arXiv},
8
+ primaryClass={cs.LG},
9
+ url={https://arxiv.org/abs/2510.15511},
10
+ }
11
+
12
+ @inproceedings{vaswani2017attention,
13
+ author = {Vaswani, Ashish and Shazeer, Noam and Parmar, Niki and Uszkoreit, Jakob and Jones, Llion and Gomez, Aidan N and Kaiser, Lukasz and Polosukhin, Illia},
14
+ booktitle = {Advances in Neural Information Processing Systems},
15
+ editor = {I. Guyon and U. Von Luxburg and S. Bengio and H. Wallach and R. Fergus and S. Vishwanathan and R. Garnett},
16
+ pages = {},
17
+ publisher = {Curran Associates, Inc.},
18
+ title = {Attention is All you Need},
19
+ url = {https://proceedings.neurips.cc/paper_files/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf},
20
+ volume = {30},
21
+ year = {2017}
22
+ }
23
+
24
+ @inproceedings{wei2022finetuned,
25
+ title={Finetuned Language Models are Zero-Shot Learners},
26
+ author={Jason Wei and Maarten Bosma and Vincent Zhao and Kelvin Guu and Adams Wei Yu and Brian Lester and Nan Du and Andrew M. Dai and Quoc V Le},
27
+ booktitle={International Conference on Learning Representations},
28
+ year={2022},
29
+ url={https://openreview.net/forum?id=gEZrGCozdqR}
30
+ }
31
+
32
+ @article{wei2022emergent,
33
+ title={Emergent Abilities of Large Language Models},
34
+ author={Jason Wei and Yi Tay and Rishi Bommasani and Colin Raffel and Barret Zoph and Sebastian Borgeaud and Dani Yogatama and Maarten Bosma and Denny Zhou and Donald Metzler and Ed H. Chi and Tatsunori Hashimoto and Oriol Vinyals and Percy Liang and Jeff Dean and William Fedus},
35
+ journal={Transactions on Machine Learning Research},
36
+ issn={2835-8856},
37
+ year={2022},
38
+ url={https://openreview.net/forum?id=yzkSU5zdwD},
39
+ note={Survey Certification}
40
+ }
41
+
42
+ @inproceedings{parikh-etal-2016-decomposable,
43
+ title = {A Decomposable Attention Model for Natural Language Inference},
44
+ author = {Parikh, Ankur and
45
+ T{\"a}ckstr{\"o}m, Oscar and
46
+ Das, Dipanjan and
47
+ Uszkoreit, Jakob},
48
+ editor = {Su, Jian and
49
+ Duh, Kevin and
50
+ Carreras, Xavier},
51
+ booktitle = {Proceedings of the 2016 Conference on Empirical Methods in Natural Language Processing},
52
+ month = nov,
53
+ year = {2016},
54
+ address = {Austin, Texas},
55
+ publisher = {Association for Computational Linguistics},
56
+ url = {https://aclanthology.org/D16-1244/},
57
+ doi = {10.18653/v1/D16-1244},
58
+ pages = {2249--2255}
59
+ }
60
+
61
+ @inproceedings{kojima2022large,
62
+ title={Large Language Models are Zero-Shot Reasoners},
63
+ author={Takeshi Kojima and Shixiang Shane Gu and Machel Reid and Yutaka Matsuo and Yusuke Iwasawa},
64
+ booktitle={Advances in Neural Information Processing Systems},
65
+ editor={Alice H. Oh and Alekh Agarwal and Danielle Belgrave and Kyunghyun Cho},
66
+ year={2022},
67
+ url={https://openreview.net/forum?id=e2TBb5y0yFf}
68
+ }
69
+
70
+ @inproceedings{bahdanau2015neural,
71
+ author = {Dzmitry Bahdanau and
72
+ Kyunghyun Cho and
73
+ Yoshua Bengio},
74
+ editor = {Yoshua Bengio and
75
+ Yann LeCun},
76
+ title = {Neural Machine Translation by Jointly Learning to Align and Translate},
77
+ booktitle = {3rd International Conference on Learning Representations, {ICLR} 2015,
78
+ San Diego, CA, USA, May 7-9, 2015, Conference Track Proceedings},
79
+ year = {2015},
80
+ url = {http://arxiv.org/abs/1409.0473},
81
+ timestamp = {Wed, 17 Jul 2019 10:40:54 +0200},
82
+ biburl = {https://dblp.org/rec/journals/corr/BahdanauCB14.bib},
83
+ bibsource = {dblp computer science bibliography, https://dblp.org}
84
+ }
85
+
86
+ % Training
87
+ @inproceedings{hu2022lora,
88
+ title={Lo{RA}: Low-Rank Adaptation of Large Language Models},
89
+ author={Edward J Hu and yelong shen and Phillip Wallis and Zeyuan Allen-Zhu and Yuanzhi Li and Shean Wang and Lu Wang and Weizhu Chen},
90
+ booktitle={International Conference on Learning Representations},
91
+ year={2022},
92
+ url={https://openreview.net/forum?id=nZeVKeeFYf9}
93
+ }
94
+
95
+ % Inference-time
96
+ @inproceedings{wei2022chain,
97
+ title={Chain of Thought Prompting Elicits Reasoning in Large Language Models},
98
+ author={Jason Wei and Xuezhi Wang and Dale Schuurmans and Maarten Bosma and brian ichter and Fei Xia and Ed H. Chi and Quoc V Le and Denny Zhou},
99
+ booktitle={Advances in Neural Information Processing Systems},
100
+ editor={Alice H. Oh and Alekh Agarwal and Danielle Belgrave and Kyunghyun Cho},
101
+ year={2022},
102
+ url={https://openreview.net/forum?id=\_VjQlMeSB\_J}
103
+ }
104
+
105
+ @misc{hu2024understanding,
106
+ title={Understanding Reasoning in Chain-of-Thought from the Hopfieldian View},
107
+ author={Lijie Hu and Liang Liu and Shu Yang and Xin CHEN and Zhen Tan and Muhammad Asif Ali and Mengdi Li and Di Wang},
108
+ year={2024},
109
+ url={https://openreview.net/forum?id=OclSRDktp3}
110
+ }
111
+
112
+ @inproceedings{zhao-etal-2024-enhancing-zero,
113
+ title = "Enhancing Zero-Shot Chain-of-Thought Reasoning in Large Language Models through Logic",
114
+ author = "Zhao, Xufeng and
115
+ Li, Mengdi and
116
+ Lu, Wenhao and
117
+ Weber, Cornelius and
118
+ Lee, Jae Hee and
119
+ Chu, Kun and
120
+ Wermter, Stefan",
121
+ editor = "Calzolari, Nicoletta and
122
+ Kan, Min-Yen and
123
+ Hoste, Veronique and
124
+ Lenci, Alessandro and
125
+ Sakti, Sakriani and
126
+ Xue, Nianwen",
127
+ booktitle = "Proceedings of the 2024 Joint International Conference on Computational Linguistics, Language Resources and Evaluation (LREC-COLING 2024)",
128
+ month = may,
129
+ year = "2024",
130
+ address = "Torino, Italia",
131
+ publisher = "ELRA and ICCL",
132
+ url = "https://aclanthology.org/2024.lrec-main.543/",
133
+ pages = "6144--6166"
134
+ }
135
+
136
+ @inproceedings{li2023inferencetime,
137
+ title={Inference-Time Intervention: Eliciting Truthful Answers from a Language Model},
138
+ author={Kenneth Li and Oam Patel and Fernanda Vi{\'e}gas and Hanspeter Pfister and Martin Wattenberg},
139
+ booktitle={Thirty-seventh Conference on Neural Information Processing Systems},
140
+ year={2023},
141
+ url={https://openreview.net/forum?id=aLLuYpn83y}
142
+ }
143
+
144
+ @article{dutta2024how,
145
+ title={How to think step-by-step: A mechanistic understanding of chain-of-thought reasoning},
146
+ author={Subhabrata Dutta and Joykirat Singh and Soumen Chakrabarti and Tanmoy Chakraborty},
147
+ journal={Transactions on Machine Learning Research},
148
+ issn={2835-8856},
149
+ year={2024},
150
+ url={https://openreview.net/forum?id=uHLDkQVtyC},
151
+ note={}
152
+ }
153
+
154
+
155
+ % Mechanistic Interpretability
156
+ @article{olah2017feature,
157
+ author = {Olah, Chris and Mordvintsev, Alexander and Schubert, Ludwig},
158
+ title = {Feature Visualization},
159
+ journal = {Distill},
160
+ year = {2017},
161
+ url = {https://distill.pub/2017/feature-visualization},
162
+ doi = {10.23915/distill.00007}
163
+ }
164
+
165
+ @article{goh2021multimodal,
166
+ author = {Goh, Gabriel and Cammarata, Nick and Voss, Chelsea and Carter, Shan and Petrov, Michael and Schubert, Ludwig and Radford, Alec and Olah, Chris},
167
+ title = {Multimodal Neurons in Artificial Neural Networks},
168
+ journal = {Distill},
169
+ year = {2021},
170
+ url = {https://distill.pub/2021/multimodal-neurons},
171
+ doi = {10.23915/distill.00030}
172
+ }
173
+
174
+ @article{schubert2021high-low,
175
+ author = {Schubert, Ludwig and Voss, Chelsea and Cammarata, Nick and Goh, Gabriel and Olah, Chris},
176
+ title = {High-Low Frequency Detectors},
177
+ journal = {Distill},
178
+ year = {2021},
179
+ url = {https://distill.pub/2020/circuits/frequency-edges},
180
+ doi = {10.23915/distill.00024.005}
181
+ }
182
+
183
+ @article{carter2019activation,
184
+ author = {Carter, Shan and Armstrong, Zan and Schubert, Ludwig and Johnson, Ian and Olah, Chris},
185
+ title = {Activation Atlas},
186
+ journal = {Distill},
187
+ year = {2019},
188
+ url = {https://distill.pub/2019/activation-atlas},
189
+ doi = {10.23915/distill.00015}
190
+ }
191
+
192
+ @inproceedings{oikarinen2025evaluating,
193
+ title={Evaluating Neuron Explanations: A Unified Framework with Sanity Checks},
194
+ author={Oikarinen, Tuomas and Yan, Ge and Weng, {Tsui-Wei}},
195
+ booktitle={Forty-second International Conference on Machine Learning},
196
+ year={2025},
197
+ publisher={PMLR},
198
+ url={https://openreview.net/forum?id=8unyWZ14mf}
199
+ }
200
+
201
+ @misc{cloud2024gradientroutingmaskinggradients,
202
+ title={Gradient Routing: Masking Gradients to Localize Computation in Neural Networks},
203
+ author={Alex Cloud and Jacob Goldman-Wetzler and EvΕΎen Wybitul and Joseph Miller and Alexander Matt Turner},
204
+ year={2024},
205
+ eprint={2410.04332},
206
+ archivePrefix={arXiv},
207
+ primaryClass={cs.LG},
208
+ url={https://arxiv.org/abs/2410.04332},
209
+ }
210
+
211
+ @misc{choi2024automatic,
212
+ author = {Choi, Dami and Huang, Vincent and Meng, Kevin and Johnson, Daniel D and Steinhardt, Jacob and Schwettmann, Sarah},
213
+ title = {Scaling Automatic Neuron Description},
214
+ year = {2024},
215
+ month = {October},
216
+ day = {23},
217
+ howpublished = {\url{https://transluce.org/neuron-descriptions}}
218
+ }
219
+
220
+ @article{olsson2022context,
221
+ title={In-context Learning and Induction Heads},
222
+ author={Olsson, Catherine and Elhage, Nelson and Nanda, Neel and Joseph, Nicholas and DasSarma, Nova and Henighan, Tom and Mann, Ben and Askell, Amanda and Bai, Yuntao and Chen, Anna and Conerly, Tom and Drain, Dawn and Ganguli, Deep and Hatfield-Dodds, Zac and Hernandez, Danny and Johnston, Scott and Jones, Andy and Kernion, Jackson and Lovitt, Liane and Ndousse, Kamal and Amodei, Dario and Brown, Tom and Clark, Jack and Kaplan, Jared and McCandlish, Sam and Olah, Chris},
223
+ year={2022},
224
+ journal={Transformer Circuits Thread},
225
+ note={https://transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html}
226
+ }
227
+
228
+ @article{mikolov2013linguistic,
229
+ title={Efficient Estimation of Word Representations in Vector Space},
230
+ author={Mikolov, Tomas and Chen, Kai and Corrado, Greg and Dean, Jeffrey},
231
+ journal={arXiv preprint arXiv:1301.3781},
232
+ year={2013},
233
+ url={https://arxiv.org/abs/1301.3781}
234
+ }
235
+
236
+
237
+ % Circuit
238
+ @article{olah2020zoom,
239
+ author = {Olah, Chris and Cammarata, Nick and Schubert, Ludwig and Goh, Gabriel and Petrov, Michael and Carter, Shan},
240
+ title = {Zoom In: An Introduction to Circuits},
241
+ journal = {Distill},
242
+ year = {2020},
243
+ note = {https://distill.pub/2020/circuits/zoom-in},
244
+ doi = {10.23915/distill.00024.001}
245
+ }
246
+
247
+ @article{elhage2021mathematical,
248
+ title={A Mathematical Framework for Transformer Circuits},
249
+ author={Elhage, Nelson and Nanda, Neel and Olsson, Catherine and Henighan, Tom and Joseph, Nicholas and Mann, Ben and Askell, Amanda and Bai, Yuntao and Chen, Anna and Conerly, Tom and DasSarma, Nova and Drain, Dawn and Ganguli, Deep and Hatfield-Dodds, Zac and Hernandez, Danny and Jones, Andy and Kernion, Jackson and Lovitt, Liane and Ndousse, Kamal and Amodei, Dario and Brown, Tom and Clark, Jack and Kaplan, Jared and McCandlish, Sam and Olah, Chris},
250
+ year={2021},
251
+ journal={Transformer Circuits Thread},
252
+ note={https://transformer-circuits.pub/2021/framework/index.html}
253
+ }
254
+ @inproceedings{marks2025sparse,
255
+ title={Sparse Feature Circuits: Discovering and Editing Interpretable Causal Graphs in Language Models},
256
+ author={Samuel Marks and Can Rager and Eric J Michaud and Yonatan Belinkov and David Bau and Aaron Mueller},
257
+ booktitle={The Thirteenth International Conference on Learning Representations},
258
+ year={2025},
259
+ url={https://openreview.net/forum?id=I4e82CIDxv}
260
+ }
261
+
262
+
263
+ @article{lindsey2025landscape,
264
+ author={Lindsey, Jack and Ameisen, Emmanuel and Nanda, Neel and Shabalin, Stepan and Piotrowski, Mateusz and McGrath, Tom and Hanna, Michael and Lewis, Owen and Tigges, Curt and Merullo, Jack and Watts, Connor and Paulo, GonΓ§alo and Batson, Joshua and Gorton, Liv and Simon, Elana and Loeffler, Max and McDougall, Callum and Lin, Johnny},
265
+ title={The Circuits Research Landscape: Results and Perspectives},
266
+ journal={Neuronpedia},
267
+ year={2025},
268
+ url={https://neuronpedia.org/graph/info}
269
+ }
270
+
271
+ @inproceedings{ferrao2025fsrl,
272
+ title={The Anatomy of Alignment: Decomposing Preference Optimization by Steering Sparse Features},
273
+ author={Jeremias Lino Ferrao and Matthijs van der Lende and Ilija Lichkovski and Clement Neo},
274
+ booktitle={Mechanistic Interpretability Workshop at NeurIPS 2025},
275
+ year={2025},
276
+ url={https://openreview.net/forum?id=CoaaltVlx7}
277
+ }
278
+
279
+ @inproceedings{sinii2025small,
280
+ title={Small Vectors, Big Effects: A Mechanistic Study of {RL}-Induced Reasoning via Steering Vectors},
281
+ author={Viacheslav Sinii and Nikita Balagansky and Yaroslav Aksenov and Vadim Kurochkin and Daniil Laptev and Alexey Gorbatovski and Boris Shaposhnikov and Daniil Gavrilov},
282
+ booktitle={Mechanistic Interpretability Workshop at NeurIPS 2025},
283
+ year={2025},
284
+ url={https://openreview.net/forum?id=M8WDG1TfBb}
285
+ }
286
+
287
+ @inproceedings{tenney-etal-2019-bert,
288
+ title = "{BERT} Rediscovers the Classical {NLP} Pipeline",
289
+ author = "Tenney, Ian and
290
+ Das, Dipanjan and
291
+ Pavlick, Ellie",
292
+ editor = "Korhonen, Anna and
293
+ Traum, David and
294
+ M{\`a}rquez, Llu{\'i}s",
295
+ booktitle = "Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics",
296
+ month = jul,
297
+ year = "2019",
298
+ address = "Florence, Italy",
299
+ publisher = "Association for Computational Linguistics",
300
+ url = "https://aclanthology.org/P19-1452/",
301
+ doi = "10.18653/v1/P19-1452",
302
+ pages = "4593--4601",
303
+ abstract = "Pre-trained text encoders have rapidly advanced the state of the art on many NLP tasks. We focus on one such model, BERT, and aim to quantify where linguistic information is captured within the network. We find that the model represents the steps of the traditional NLP pipeline in an interpretable and localizable way, and that the regions responsible for each step appear in the expected sequence: POS tagging, parsing, NER, semantic roles, then coreference. Qualitative analysis reveals that the model can and often does adjust this pipeline dynamically, revising lower-level decisions on the basis of disambiguating information from higher-level representations."
304
+ }
305
+
306
+
307
+ @inproceedings{cho2025corrsteer,
308
+ title={CorrSteer: Generation-Time {LLM} Steering via Correlated Sparse Autoencoder Features},
309
+ author={Cho, Seonglae and Wu, Zekun and Koshiyama, Adriano},
310
+ booktitle={Forty-third International Conference on Machine Learning},
311
+ year={2026}
312
+ }
313
+
314
+ @inproceedings{conmy2023towards,
315
+ title={Towards Automated Circuit Discovery for Mechanistic Interpretability},
316
+ author={Arthur Conmy and Augustine N. Mavor-Parker and Aengus Lynch and Stefan Heimersheim and Adri{\`a} Garriga-Alonso},
317
+ booktitle={Thirty-seventh Conference on Neural Information Processing Systems},
318
+ year={2023},
319
+ url={https://openreview.net/forum?id=89ia77nZ8u}
320
+ }
321
+
322
+ @article{lindsey2025biology,
323
+ author={Lindsey, Jack and Gurnee, Wes and Ameisen, Emmanuel and Chen, Brian and Pearce, Adam and Turner, Nicholas L. and Citro, Craig and Abrahams, David and Carter, Shan and Hosmer, Basil and Marcus, Jonathan and Sklar, Michael and Templeton, Adly and Bricken, Trenton and McDougall, Callum and Cunningham, Hoagy and Henighan, Thomas and Jermyn, Adam and Jones, Andy and Persic, Andrew and Qi, Zhenyi and Thompson, T. Ben and Zimmerman, Sam and Rivoire, Kelley and Conerly, Thomas and Olah, Chris and Batson, Joshua},
324
+ title={On the Biology of a Large Language Model},
325
+ journal={Transformer Circuits Thread},
326
+ year={2025},
327
+ url={https://transformer-circuits.pub/2025/attribution-graphs/biology.html}
328
+ }
329
+
330
+ @article{ameisen2025circuit,
331
+ author={Ameisen, Emmanuel and Lindsey, Jack and Pearce, Adam and Gurnee, Wes and Turner, Nicholas L. and Chen, Brian and Citro, Craig and Abrahams, David and Carter, Shan and Hosmer, Basil and Marcus, Jonathan and Sklar, Michael and Templeton, Adly and Bricken, Trenton and McDougall, Callum and Cunningham, Hoagy and Henighan, Thomas and Jermyn, Adam and Jones, Andy and Persic, Andrew and Qi, Zhenyi and Ben Thompson, T. and Zimmerman, Sam and Rivoire, Kelley and Conerly, Thomas and Olah, Chris and Batson, Joshua},
332
+ title={Circuit Tracing: Revealing Computational Graphs in Language Models},
333
+ journal={Transformer Circuits Thread},
334
+ year={2025},
335
+ url={https://transformer-circuits.pub/2025/attribution-graphs/methods.html}
336
+ }
337
+
338
+ @inproceedings{sun-2025-circuit,
339
+ title = "Circuit Stability Characterizes Language Model Generalization",
340
+ author = "Sun, Alan",
341
+ editor = "Che, Wanxiang and
342
+ Nabende, Joyce and
343
+ Shutova, Ekaterina and
344
+ Pilehvar, Mohammad Taher",
345
+ booktitle = "Proceedings of the 63rd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)",
346
+ month = jul,
347
+ year = "2025",
348
+ address = "Vienna, Austria",
349
+ publisher = "Association for Computational Linguistics",
350
+ url = "https://aclanthology.org/2025.acl-long.442/",
351
+ doi = "10.18653/v1/2025.acl-long.442",
352
+ pages = "9025--9040",
353
+ ISBN = "979-8-89176-251-0",
354
+ }
355
+
356
+ @inproceedings{syed-etal-2024-attribution,
357
+ title = "Attribution Patching Outperforms Automated Circuit Discovery",
358
+ author = "Syed, Aaquib and
359
+ Rager, Can and
360
+ Conmy, Arthur",
361
+ editor = "Belinkov, Yonatan and
362
+ Kim, Najoung and
363
+ Jumelet, Jaap and
364
+ Mohebbi, Hosein and
365
+ Mueller, Aaron and
366
+ Chen, Hanjie",
367
+ booktitle = "Proceedings of the 7th BlackboxNLP Workshop: Analyzing and Interpreting Neural Networks for NLP",
368
+ month = nov,
369
+ year = "2024",
370
+ address = "Miami, Florida, US",
371
+ publisher = "Association for Computational Linguistics",
372
+ url = "https://aclanthology.org/2024.blackboxnlp-1.25/",
373
+ doi = "10.18653/v1/2024.blackboxnlp-1.25",
374
+ pages = "407--416",
375
+ }
376
+
377
+
378
+ % Superposition
379
+ @article{elhage2022superposition,
380
+ title = {Toy Models of Superposition},
381
+ author = {Elhage, Nelson and Hume, Tristan and Olsson, Catherine and Schiefer, Nicholas and Henighan, Tom and Kravec, Shauna and Hatfield-Dodds, Zac and Lasenby, Robert and Drain, Dawn and Chen, Carol and Grosse, Roger and McCandlish, Sam and Kaplan, Jared and Amodei, Dario and Wattenberg, Martin and Olah, Christopher},
382
+ year = {2022},
383
+ journal = {Transformer Circuits Thread},
384
+ url = {https://transformer-circuits.pub/2022/toy\_model/index.html}
385
+ }
386
+
387
+ @ARTICLE{dongho2006compressed,
388
+ author={Donoho, D.L.},
389
+ journal={IEEE Transactions on Information Theory},
390
+ title={Compressed sensing},
391
+ year={2006},
392
+ volume={52},
393
+ number={4},
394
+ pages={1289-1306},
395
+ keywords={Compressed sensing;Image reconstruction;Pixel;Vectors;Digital images;Image coding;Transform coding;Size measurement;Signal processing;Data mining;Adaptive sampling;almost-spherical sections of Banach spaces;Basis Pursuit;eigenvalues of random matrices;Gel'fand;information-based complexity;integrated sensing and processing;minimum;optimal recovery;Quotient-of-a-Subspace theorem;sparse solution of linear equations},
396
+ doi={10.1109/TIT.2006.871582}
397
+ }
398
+
399
+ @inproceedings{bora2017compressed,
400
+ title={Compressed Sensing using Generative Models},
401
+ author={Bora, Ashish and Jalal, Ajil and Price, Eric and Dimakis, Alexandros G},
402
+ booktitle={Proceedings of the 34th International Conference on Machine Learning (ICML)},
403
+ editor={Doina Precup and Yee Whye Teh},
404
+ volume={70},
405
+ series={Proceedings of Machine Learning Research},
406
+ pages={537--546},
407
+ year={2017},
408
+ publisher={PMLR},
409
+ address={Sydney, Australia},
410
+ url={https://proceedings.mlr.press/v70/bora17a/bora17a.pdf}
411
+ }
412
+
413
+
414
+ % Linear Representation
415
+ @inproceedings{faruqui-etal-2015-sparse,
416
+ title = {Sparse Overcomplete Word Vector Representations},
417
+ author = {Faruqui, Manaal and
418
+ Tsvetkov, Yulia and
419
+ Yogatama, Dani and
420
+ Dyer, Chris and
421
+ Smith, Noah A.},
422
+ editor = {Zong, Chengqing and
423
+ Strube, Michael},
424
+ booktitle = {Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics and the 7th International Joint Conference on Natural Language Processing (Volume 1: Long Papers)},
425
+ month = jul,
426
+ year = {2015},
427
+ address = {Beijing, China},
428
+ publisher = {Association for Computational Linguistics},
429
+ url = {https://aclanthology.org/P15-1144/},
430
+ doi = {10.3115/v1/P15-1144},
431
+ pages = {1491--1500}
432
+ }
433
+
434
+ @inproceedings{socher-etal-2013-recursive,
435
+ title = "Recursive Deep Models for Semantic Compositionality Over a Sentiment Treebank",
436
+ author = "Socher, Richard and
437
+ Perelygin, Alex and
438
+ Wu, Jean and
439
+ Chuang, Jason and
440
+ Manning, Christopher D. and
441
+ Ng, Andrew and
442
+ Potts, Christopher",
443
+ editor = "Yarowsky, David and
444
+ Baldwin, Timothy and
445
+ Korhonen, Anna and
446
+ Livescu, Karen and
447
+ Bethard, Steven",
448
+ booktitle = "Proceedings of the 2013 Conference on Empirical Methods in Natural Language Processing",
449
+ month = oct,
450
+ year = "2013",
451
+ address = "Seattle, Washington, USA",
452
+ publisher = "Association for Computational Linguistics",
453
+ url = "https://aclanthology.org/D13-1170/",
454
+ pages = "1631--1642"
455
+ }
456
+
457
+ @article{arora-etal-2018-linear,
458
+ title = {Linear Algebraic Structure of Word Senses, with Applications to Polysemy},
459
+ author = {Arora, Sanjeev and
460
+ Li, Yuanzhi and
461
+ Liang, Yingyu and
462
+ Ma, Tengyu and
463
+ Risteski, Andrej},
464
+ editor = {Lee, Lillian and
465
+ Johnson, Mark and
466
+ Toutanova, Kristina and
467
+ Roark, Brian},
468
+ journal = {Transactions of the Association for Computational Linguistics},
469
+ volume = {6},
470
+ year = {2018},
471
+ address = {Cambridge, MA},
472
+ publisher = {MIT Press},
473
+ url = {https://aclanthology.org/Q18-1034/},
474
+ doi = {10.1162/tacl_a_00034},
475
+ pages = {483--495}
476
+ }
477
+
478
+ @inproceedings{park2023the,
479
+ title={The Linear Representation Hypothesis and the Geometry of Large Language Models},
480
+ author={Kiho Park and Yo Joong Choe and Victor Veitch},
481
+ booktitle={Causal Representation Learning Workshop at NeurIPS 2023},
482
+ year={2023},
483
+ url={https://openreview.net/forum?id=T0PoOJg8cK}
484
+ }
485
+
486
+ @inproceedings{park2024the,
487
+ title={The Geometry of Categorical and Hierarchical Concepts in Large Language Models},
488
+ author={Kiho Park and Yo Joong Choe and Yibo Jiang and Victor Veitch},
489
+ booktitle={ICML 2024 Workshop on Mechanistic Interpretability},
490
+ year={2024},
491
+ url={https://openreview.net/forum?id=KXuYjuBzKo}
492
+ }
493
+
494
+
495
+ % Steering Vector
496
+ @inproceedings{subramani-etal-2022-extracting,
497
+ title = "Extracting Latent Steering Vectors from Pretrained Language Models",
498
+ author = "Subramani, Nishant and
499
+ Suresh, Nivedita and
500
+ Peters, Matthew",
501
+ editor = "Muresan, Smaranda and
502
+ Nakov, Preslav and
503
+ Villavicencio, Aline",
504
+ booktitle = "Findings of the Association for Computational Linguistics: ACL 2022",
505
+ month = may,
506
+ year = "2022",
507
+ address = "Dublin, Ireland",
508
+ publisher = "Association for Computational Linguistics",
509
+ url = "https://aclanthology.org/2022.findings-acl.48/",
510
+ doi = "10.18653/v1/2022.findings-acl.48",
511
+ pages = "566--581"
512
+ }
513
+
514
+ @misc{azizi2025activationsteeringchainofthoughtcompression,
515
+ title={Activation Steering for Chain-of-Thought Compression},
516
+ author={Seyedarmin Azizi and Erfan Baghaei Potraghloo and Massoud Pedram},
517
+ year={2025},
518
+ eprint={2507.04742},
519
+ archivePrefix={arXiv},
520
+ primaryClass={cs.AI},
521
+ url={https://arxiv.org/abs/2507.04742},
522
+ }
523
+
524
+ @inproceedings{tan2024analysing,
525
+ title={Analysing the Generalisation and Reliability of Steering Vectors},
526
+ author={Daniel Chee Hian Tan and David Chanin and Aengus Lynch and Brooks Paige and Dimitrios Kanoulas and Adri{\`a} Garriga-Alonso and Robert Kirk},
527
+ booktitle={The Thirty-eighth Annual Conference on Neural Information Processing Systems},
528
+ year={2024},
529
+ url={https://openreview.net/forum?id=v8X70gTodR}
530
+ }
531
+
532
+ @inproceedings{qiu2024spectral,
533
+ title={Spectral Editing of Activations for Large Language Model Alignment},
534
+ author={Yifu QIU and Zheng Zhao and Yftah Ziser and Anna Korhonen and Edoardo Ponti and Shay B Cohen},
535
+ booktitle={The Thirty-eighth Annual Conference on Neural Information Processing Systems},
536
+ year={2024},
537
+ url={https://openreview.net/forum?id=pqYceEa87j}
538
+ }
539
+
540
+
541
+ @inproceedings{luo2024pace,
542
+ title={Pa{CE}: Parsimonious Concept Engineering for Large Language Models},
543
+ author={Jinqi Luo and Tianjiao Ding and Kwan Ho Ryan Chan and Darshan Thaker and Aditya Chattopadhyay and Chris Callison-Burch and Rene Vidal},
544
+ booktitle={The Thirty-eighth Annual Conference on Neural Information Processing Systems},
545
+ year={2024},
546
+ url={https://openreview.net/forum?id=lOMHt16T8R}
547
+ }
548
+
549
+
550
+ @inproceedings{rimsky-etal-2024-steering,
551
+ title = "Steering Llama 2 via Contrastive Activation Addition",
552
+ author = "Rimsky, Nina and
553
+ Gabrieli, Nick and
554
+ Schulz, Julian and
555
+ Tong, Meg and
556
+ Hubinger, Evan and
557
+ Turner, Alexander",
558
+ editor = "Ku, Lun-Wei and
559
+ Martins, Andre and
560
+ Srikumar, Vivek",
561
+ booktitle = "Proceedings of the 62nd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)",
562
+ month = aug,
563
+ year = "2024",
564
+ address = "Bangkok, Thailand",
565
+ publisher = "Association for Computational Linguistics",
566
+ url = "https://aclanthology.org/2024.acl-long.828/",
567
+ doi = "10.18653/v1/2024.acl-long.828",
568
+ pages = "15504--15522"
569
+ }
570
+
571
+ @inproceedings{hernandez2024inspecting,
572
+ title={Inspecting and Editing Knowledge Representations in Language Models},
573
+ author={Evan Hernandez and Belinda Z. Li and Jacob Andreas},
574
+ booktitle={First Conference on Language Modeling},
575
+ year={2024},
576
+ url={https://openreview.net/forum?id=ADtL6fgNRv}
577
+ }
578
+
579
+ @article{DBLP:journals/corr/abs-2308-10248,
580
+ publtype={informal},
581
+ author={Alexander Matt Turner and Lisa Thiergart and David Udell and Gavin Leech and Ulisse Mini and Monte MacDiarmid},
582
+ title={Activation Addition: Steering Language Models Without Optimization},
583
+ year={2023},
584
+ cdate={1672531200000},
585
+ journal={CoRR},
586
+ volume={abs/2308.10248},
587
+ url={https://doi.org/10.48550/arXiv.2308.10248}
588
+ }
589
+
590
+ @inproceedings{konen-etal-2024-style,
591
+ title = {Style Vectors for Steering Generative Large Language Models},
592
+ author = {Konen, Kai and
593
+ Jentzsch, Sophie and
594
+ Diallo, Diaoul{\'e} and
595
+ Sch{\"u}tt, Peer and
596
+ Bensch, Oliver and
597
+ El Baff, Roxanne and
598
+ Opitz, Dominik and
599
+ Hecking, Tobias},
600
+ editor = {Graham, Yvette and
601
+ Purver, Matthew},
602
+ booktitle = {Findings of the Association for Computational Linguistics: EACL 2024},
603
+ month = mar,
604
+ year = {2024},
605
+ address = {St. Julian{'}s, Malta},
606
+ publisher = {Association for Computational Linguistics},
607
+ url = {https://aclanthology.org/2024.findings-eacl.52/},
608
+ pages = {782--802}
609
+ }
610
+
611
+ % Attention Sink
612
+ @inproceedings{xiao2024efficient,
613
+ title={Efficient Streaming Language Models with Attention Sinks},
614
+ author={Guangxuan Xiao and Yuandong Tian and Beidi Chen and Song Han and Mike Lewis},
615
+ booktitle={The Twelfth International Conference on Learning Representations},
616
+ year={2024},
617
+ url={https://openreview.net/forum?id=NG7sS51zVF}
618
+ }
619
+
620
+ @article{hazra2025under,
621
+ title={Under the Hood of a Reasoning Model},
622
+ author={Hazra, Dron and Loeffler, Max and Cubuktepe, Murat and Avagyan, Levon and Gorton, Liv and Bissell, Mark and Lewis, Owen and McGrath, Thomas and Balsam, Daniel},
623
+ journal={Goodfire Research Blog},
624
+ year={2025},
625
+ month={Apr},
626
+ day={15},
627
+ note={\url{https://goodfire.ai/blog/under-the-hood-of-a-reasoning-model}},
628
+ }
629
+
630
+ @inproceedings{gu2025when,
631
+ title={When Attention Sink Emerges in Language Models: An Empirical View},
632
+ author={Xiangming Gu and Tianyu Pang and Chao Du and Qian Liu and Fengzhuo Zhang and Cunxiao Du and Ye Wang and Min Lin},
633
+ booktitle={The Thirteenth International Conference on Learning Representations},
634
+ year={2025},
635
+ url={https://openreview.net/forum?id=78Nn4QJTEN}
636
+ }
637
+
638
+
639
+ % Sparse Coding
640
+ @article{olshausen1997sparse,
641
+ title={Sparse coding with an overcomplete basis set: A strategy employed by V1?},
642
+ author={Olshausen, Bruno A. and Field, David J.},
643
+ journal={Vision Research},
644
+ volume={37},
645
+ number={23},
646
+ pages={3311--3325},
647
+ year={1997},
648
+ publisher={Elsevier}
649
+ }
650
+
651
+ @book{elad2010sparse,
652
+ title={Sparse and Redundant Representations: From Theory to Applications in Signal and Image Processing},
653
+ author={Elad, Michael},
654
+ year={2010},
655
+ publisher={Springer New York},
656
+ doi={10.1007/978-1-4419-7011-4},
657
+ isbn={978-1-4419-7010-7},
658
+ edition={1},
659
+ pages={XX, 376}
660
+ }
661
+
662
+ @article{braun2024identifying,
663
+ title={Identifying functionally important features with end-to-end sparse dictionary learning},
664
+ author={Braun, Dan and Taylor, Jordan and Goldowsky-Dill, Nicholas and Sharkey, Lee},
665
+ journal={Advances in Neural Information Processing Systems},
666
+ volume={37},
667
+ pages={107286--107325},
668
+ year={2024}
669
+ }
670
+
671
+
672
+ % SAE
673
+ @inproceedings{huben2024sparse,
674
+ title = {Sparse Autoencoders Find Highly Interpretable Features in Language Models},
675
+ author = {Robert Huben and Hoagy Cunningham and Logan Riggs Smith and Aidan Ewart and Lee Sharkey},
676
+ booktitle = {The Twelfth International Conference on Learning Representations},
677
+ year = {2024},
678
+ url = {https://openreview.net/forum?id=F76bwRSLeK}
679
+ }
680
+
681
+ @inproceedings{leask2025inferencetime,
682
+ title={Inference-Time Decomposition of Activations ({ITDA}): A Scalable Approach to Interpreting Large Language Models},
683
+ author={Patrick Leask and Neel Nanda and Noura Al Moubayed},
684
+ booktitle={Forty-second International Conference on Machine Learning},
685
+ year={2025},
686
+ url={https://openreview.net/forum?id=4WMZqAoYi4}
687
+ }
688
+
689
+ @article{templeton2024scaling,
690
+ title = {Scaling Monosemanticity: Extracting Interpretable Features from Claude 3 Sonnet},
691
+ author = {Templeton, Adly and Conerly, Tom and Marcus, Jonathan and Lindsey, Jack and Bricken, Trenton and Chen, Brian and Pearce, Adam and Citro, Craig and Ameisen, Emmanuel and Jones, Andy and Cunningham, Hoagy and Turner, Nicholas L and McDougall, Callum and MacDiarmid, Monte and Freeman, C. Daniel and Sumers, Theodore R. and Rees, Edward and Batson, Joshua and Jermyn, Adam and Carter, Shan and Olah, Chris and Henighan, Tom},
692
+ year = {2024},
693
+ journal = {Transformer Circuits Thread},
694
+ url = {https://transformer-circuits.pub/2024/scaling-monosemanticity/index.html}
695
+ }
696
+
697
+ @article{bricken2023monosemanticity,
698
+ title={Towards Monosemanticity: Decomposing Language Models With Dictionary Learning},
699
+ author={Bricken, Trenton and Templeton, Adly and Batson, Joshua and Chen, Brian and Jermyn, Adam and Conerly, Tom and Turner, Nick and Anil, Cem and Denison, Carson and Askell, Amanda and Lasenby, Robert and Wu, Yifan and Kravec, Shauna and Schiefer, Nicholas and Maxwell, Tim and Joseph, Nicholas and Hatfield-Dodds, Zac and Tamkin, Alex and Nguyen, Karina and McLean, Brayden and Burke, Josiah E and Hume, Tristan and Carter, Shan and Henighan, Tom and Olah, Christopher},
700
+ year={2023},
701
+ journal={Transformer Circuits Thread},
702
+ note={https://transformer-circuits.pub/2023/monosemantic-features/index.html}
703
+ }
704
+
705
+ @misc{smith2025negative,
706
+ title = {Negative Results for SAEs On Downstream Tasks and Deprioritising SAE Research},
707
+ author = {Lewis Smith and Senthooran Rajamanoharan and Arthur Conmy and Callum McDougall and Tom Lieberum and JΓ‘nos KramΓ‘r and Rohin Shah and Neel Nanda},
708
+ year = {2025},
709
+ note = {DeepMind Mechanistic Interpretability Team Progress Update \#2},
710
+ howpublished = {\url{https://www.lesswrong.com/posts/4uXCAJNuPKtKBsi28/sae-progress-update-2-draft}},
711
+ }
712
+
713
+ @misc{kissane2024saefinetuning,
714
+ author = {Connor Kissane and Robert Krzyzanowski and Arthur Conmy and Neel Nanda},
715
+ title = {SAEs (usually) Transfer Between Base and Chat Models},
716
+ year = {2024},
717
+ howpublished = {Alignment Forum},
718
+ url = {https://www.alignmentforum.org/posts/fmwk6qxrpW8d4jvbd/saes-usually-transfer-between-base-and-chat-models}
719
+ }
720
+
721
+ @inproceedings{ayonrinde2024interpretability,
722
+ title={Interpretability as Compression: Reconsidering {SAE} Explanations of Neural Activations},
723
+ author={Kola Ayonrinde and Michael T Pearce and Lee Sharkey},
724
+ booktitle={NeurIPS 2024 Workshop on Scientific Methods for Understanding Deep Learning},
725
+ year={2024},
726
+ url={https://openreview.net/forum?id=hAqeEZRVSD}
727
+ }
728
+
729
+ @inproceedings{lieberum-etal-2024-gemma,
730
+ title = "Gemma Scope: Open Sparse Autoencoders Everywhere All At Once on Gemma 2",
731
+ author = "Lieberum, Tom and
732
+ Rajamanoharan, Senthooran and
733
+ Conmy, Arthur and
734
+ Smith, Lewis and
735
+ Sonnerat, Nicolas and
736
+ Varma, Vikrant and
737
+ Kramar, Janos and
738
+ Dragan, Anca and
739
+ Shah, Rohin and
740
+ Nanda, Neel",
741
+ editor = "Belinkov, Yonatan and
742
+ Kim, Najoung and
743
+ Jumelet, Jaap and
744
+ Mohebbi, Hosein and
745
+ Mueller, Aaron and
746
+ Chen, Hanjie",
747
+ booktitle = "Proceedings of the 7th BlackboxNLP Workshop: Analyzing and Interpreting Neural Networks for NLP",
748
+ month = nov,
749
+ year = "2024",
750
+ address = "Miami, Florida, US",
751
+ publisher = "Association for Computational Linguistics",
752
+ url = "https://aclanthology.org/2024.blackboxnlp-1.19/",
753
+ doi = "10.18653/v1/2024.blackboxnlp-1.19",
754
+ pages = "278--300"
755
+ }
756
+
757
+ @misc{he2024llamascopeextractingmillions,
758
+ title={Llama Scope: Extracting Millions of Features from Llama-3.1-8B with Sparse Autoencoders},
759
+ author={Zhengfu He and Wentao Shu and Xuyang Ge and Lingjie Chen and Junxuan Wang and Yunhua Zhou and Frances Liu and Qipeng Guo and Xuanjing Huang and Zuxuan Wu and Yu-Gang Jiang and Xipeng Qiu},
760
+ year={2024},
761
+ eprint={2410.20526},
762
+ archivePrefix={arXiv},
763
+ primaryClass={cs.LG},
764
+ url={https://arxiv.org/abs/2410.20526},
765
+ }
766
+
767
+ @misc{sae_finetuning,
768
+ author= {Connor Kissane and Robert Krzyzanowski and Arthur Conmy and Neel Nanda},
769
+ url = {https://www.alignmentforum.org/posts/fmwk6qxrpW8d4jvbd/saes-usually-transfer-between-base-and-chat-models},
770
+ year = {2024},
771
+ howpublished = {Alignment Forum},
772
+ title = {SAEs (usually) Transfer Between Base and Chat Models},
773
+ }
774
+
775
+ @misc{stefanhex2023residual,
776
+ title = {Residual stream norms grow exponentially over the forward pass},
777
+ author = {Stefan Heimersheim and Alex Turner},
778
+ year = {2023},
779
+ month = {May},
780
+ day = {7},
781
+ url = {https://www.lesswrong.com/posts/8mizBCm3dyc432nK8/residual-stream-norms-grow-exponentially-over-the-forward},
782
+ note = {LessWrong, AI Alignment Forum}
783
+ }
784
+
785
+
786
+ % SAE Architecture
787
+ @inproceedings{bussmann2024batchtopk,
788
+ title={BatchTopK Sparse Autoencoders},
789
+ author={Bart Bussmann and Patrick Leask and Neel Nanda},
790
+ booktitle={NeurIPS 2024 Workshop on Scientific Methods for Understanding Deep Learning},
791
+ year={2024},
792
+ url={https://openreview.net/forum?id=d4dpOCqybL}
793
+ }
794
+
795
+ @misc{rajamanoharan2024jumpingaheadimprovingreconstruction,
796
+ title={Jumping Ahead: Improving Reconstruction Fidelity with JumpReLU Sparse Autoencoders},
797
+ author={Senthooran Rajamanoharan and Tom Lieberum and Nicolas Sonnerat and Arthur Conmy and Vikrant Varma and JΓ‘nos KramΓ‘r and Neel Nanda},
798
+ year={2024},
799
+ eprint={2407.14435},
800
+ archivePrefix={arXiv},
801
+ primaryClass={cs.LG},
802
+ url={https://arxiv.org/abs/2407.14435},
803
+ }
804
+
805
+ @misc{rajamanoharan2025jumping,
806
+ title={Jumping Ahead: Improving Reconstruction Fidelity with JumpReLU Sparse Autoencoders},
807
+ author={Senthooran Rajamanoharan and Tom Lieberum and Nicolas Sonnerat and Arthur Conmy and Vikrant Varma and Janos Kramar and Neel Nanda},
808
+ year={2025},
809
+ url={https://openreview.net/forum?id=mMPaQzgzAN}
810
+ }
811
+
812
+ @inproceedings{dunefsky2024transcoders,
813
+ title={Transcoders find interpretable {LLM} feature circuits},
814
+ author={Jacob Dunefsky and Philippe Chlenski and Neel Nanda},
815
+ booktitle={The Thirty-eighth Annual Conference on Neural Information Processing Systems},
816
+ year={2024},
817
+ url={https://openreview.net/forum?id=J6zHcScAo0}
818
+ }
819
+
820
+ @inproceedings{gao2025topksae,
821
+ title = {Scaling and evaluating TopK Sparse Autoencoders},
822
+ author = {Leo Gao and Tom Dupre la Tour and Henk Tillman and Gabriel Goh and Rajan Troll and Alec Radford and Ilya Sutskever and Jan Leike and Jeffrey Wu},
823
+ booktitle = {The Thirteenth International Conference on Learning Representations},
824
+ year = {2024},
825
+ url = {https://openreview.net/forum?id=tcsZt9ZNKD}
826
+ }
827
+
828
+ @misc{lindsey2024crosscoders,
829
+ title = {Sparse Crosscoders for Cross-Layer Features and Model Diffing},
830
+ author = {Lindsey, Jack and Templeton, Adly and Marcus, Jonathan and Conerly, Thomas and Batson, Joshua and Olah, Christopher},
831
+ note = {Transformer Circuits Thread, Anthropic. Research update, not peer-reviewed.},
832
+ year = {2024},
833
+ month = {October},
834
+ url = {https://transformer-circuits.pub/2024/crosscoders/index.html}
835
+ }
836
+
837
+ @misc{valentin2025dbksvdscalablealternatingoptimization,
838
+ title={DB-KSVD: Scalable Alternating Optimization for Disentangling High-Dimensional Embedding Spaces},
839
+ author={Romeo Valentin and Sydney M. Katz and Vincent Vanhoucke and Mykel J. Kochenderfer},
840
+ year={2025},
841
+ eprint={2505.18441},
842
+ archivePrefix={arXiv},
843
+ primaryClass={cs.LG},
844
+ url={https://arxiv.org/abs/2505.18441},
845
+ }
846
+
847
+ @inproceedings{leask2024itda,
848
+ title = {Inference-Time Decomposition of Activations (ITDA): A Scalable Approach to Interpreting Large Language Models},
849
+ author = {Leask, Patrick and Nanda, Neel and Al Moubayed, Noura},
850
+ booktitle = {Proceedings of the 41st International Conference on Machine Learning (ICML)},
851
+ year = {2024},
852
+ address = {San Diego, CA, USA},
853
+ note = {Poster presentation, East Exhibition Hall A-B \#E-2410, Wed 16 Jul 11:00 a.m.--1:30 p.m. PDT},
854
+ publisher = {PMLR}
855
+ }
856
+
857
+ % SAE Feature
858
+ @misc{bricken2023simplest,
859
+ author = {Trenton Bricken and Joshua Batson and Adly Templeton and Adam Jermyn and Tom Henighan and Chris Olah},
860
+ title = {Features as The Simplest Factorization},
861
+ journal = {Transformer Circuits Thread},
862
+ year = {2023},
863
+ month = {May},
864
+ url = {https://transformer-circuits.pub/2023/may-update/index.html#simple-factorization},
865
+ note = {Part of the May 2023 Circuits Updates by the Anthropic interpretability team}
866
+ }
867
+
868
+ @misc{bloom2024understandingfeatureslogitlens,
869
+ title = {Understanding SAE Features with the Logit Lens},
870
+ author = {Joseph Bloom, Johnny Lin},
871
+ year = {2024},
872
+ url = {https://www.lesswrong.com/posts/qykrYY6rXXM7EEs8Q/understanding-sae-features-with-the-logit-lens},
873
+ }
874
+
875
+ @inproceedings{stolfo2025antipodal,
876
+ title={Antipodal Pairing and Mechanistic Signals in Dense SAE Latents},
877
+ author={Alessandro Stolfo and Ben Peng Wu and Mrinmaya Sachan},
878
+ booktitle={ICLR 2025 Workshop on Building Trust in Language Models and Applications},
879
+ year={2025},
880
+ url={https://openreview.net/forum?id=Zlx6AlEoB0}
881
+ }
882
+
883
+ @misc{heap2025sparseautoencodersinterpretrandomly,
884
+ title={Sparse Autoencoders Can Interpret Randomly Initialized Transformers},
885
+ author={Thomas Heap and Tim Lawson and Lucy Farnik and Laurence Aitchison},
886
+ year={2025},
887
+ eprint={2501.17727},
888
+ archivePrefix={arXiv},
889
+ primaryClass={cs.LG},
890
+ url={https://arxiv.org/abs/2501.17727},
891
+ }
892
+
893
+ @inproceedings{balagansky2025mechanistic,
894
+ title = {Mechanistic Permutability: Match Features Across Layers},
895
+ author = {Nikita Balagansky and Ian Maksimov and Daniil Gavrilov},
896
+ booktitle = {The Thirteenth International Conference on Learning Representations},
897
+ year = {2025},
898
+ url = {https://openreview.net/forum?id=MDvecs7EvO}
899
+ }
900
+
901
+ @misc{paulo2025sparseautoencoderstraineddata,
902
+ title={Sparse Autoencoders Trained on the Same Data Learn Different Features},
903
+ author={GonΓ§alo Paulo and Nora Belrose},
904
+ year={2025},
905
+ eprint={2501.16615},
906
+ archivePrefix={arXiv},
907
+ primaryClass={cs.LG},
908
+ url={https://arxiv.org/abs/2501.16615},
909
+ }
910
+
911
+
912
+ % SAE Steering
913
+ @inproceedings{zhao-etal-2025-steering,
914
+ title = "Steering Knowledge Selection Behaviours in {LLM}s via {SAE}-Based Representation Engineering",
915
+ author = "Zhao, Yu and
916
+ Devoto, Alessio and
917
+ Hong, Giwon and
918
+ Du, Xiaotang and
919
+ Gema, Aryo Pradipta and
920
+ Wang, Hongru and
921
+ He, Xuanli and
922
+ Wong, Kam-Fai and
923
+ Minervini, Pasquale",
924
+ editor = "Chiruzzo, Luis and
925
+ Ritter, Alan and
926
+ Wang, Lu",
927
+ booktitle = "Proceedings of the 2025 Conference of the Nations of the Americas Chapter of the Association for Computational Linguistics: Human Language Technologies (Volume 1: Long Papers)",
928
+ month = apr,
929
+ year = "2025",
930
+ address = "Albuquerque, New Mexico",
931
+ publisher = "Association for Computational Linguistics",
932
+ url = "https://aclanthology.org/2025.naacl-long.264/",
933
+ doi = "10.18653/v1/2025.naacl-long.264",
934
+ pages = "5117--5136",
935
+ ISBN = "979-8-89176-189-6"
936
+ }
937
+
938
+ @misc{liu2024incontextvectorsmakingcontext,
939
+ title={In-context Vectors: Making In Context Learning More Effective and Controllable Through Latent Space Steering},
940
+ author={Sheng Liu and Haotian Ye and Lei Xing and James Zou},
941
+ year={2024},
942
+ eprint={2311.06668},
943
+ archivePrefix={arXiv},
944
+ primaryClass={cs.LG},
945
+ url={https://arxiv.org/abs/2311.06668},
946
+ }
947
+
948
+ @misc{zhao2025steeringknowledgeselectionbehaviours,
949
+ title = {Steering Knowledge Selection Behaviours in LLMs via SAE-Based Representation Engineering},
950
+ author = {Yu Zhao and Alessio Devoto and Giwon Hong and Xiaotang Du and Aryo Pradipta Gema and Hongru Wang and Xuanli He and Kam-Fai Wong and Pasquale Minervini},
951
+ year = {2025},
952
+ eprint = {2410.15999},
953
+ archiveprefix = {arXiv},
954
+ primaryclass = {cs.CL},
955
+ url = {https://arxiv.org/abs/2410.15999}
956
+ }
957
+
958
+ @inproceedings{lee2025programming,
959
+ title={Programming Refusal with Conditional Activation Steering},
960
+ author={Bruce W. Lee and Inkit Padhi and Karthikeyan Natesan Ramamurthy and Erik Miehling and Pierre Dognin and Manish Nagireddy and Amit Dhurandhar},
961
+ booktitle={The Thirteenth International Conference on Learning Representations},
962
+ year={2025},
963
+ url={https://openreview.net/forum?id=Oi47wc10sm}
964
+ }
965
+
966
+ @article{karvonen2024sieve,
967
+ title={Sieve: SAEs Beat Baselines on a Real-World Task (A Code Generation Case Study)},
968
+ author={Karvonen, Adam and Pai, Dhruv and Wang, Mason and Keigwin, Ben},
969
+ journal={Tilde Research Blog},
970
+ year={2024},
971
+ month={12},
972
+ url={https://www.tilderesearch.com/blog/sieve},
973
+ note={Blog post}
974
+ }
975
+
976
+
977
+ @inproceedings{obrien2025steering,
978
+ title={Steering Language Model Refusal with Sparse Autoencoders},
979
+ author={Kyle O'Brien and David Majercak and Xavier Fernandes and Richard G. Edgar and Blake Bullwinkel and Jingya Chen and Harsha Nori and Dean Carignan and Eric Horvitz and Forough Poursabzi-Sangdeh},
980
+ booktitle={ICML 2025 Workshop on Reliable and Responsible Foundation Models},
981
+ year={2025},
982
+ url={https://openreview.net/forum?id=PMK1jdGQoc}
983
+ }
984
+
985
+ @misc{ferrao2024autosteer,
986
+ title = {AutoSteer: Weight-Preserving Reinforcement Learning for Interpretable Model Control},
987
+ author = {Jeremias Lino Ferrao},
988
+ date = {2024-11-25},
989
+ year = {2024},
990
+ organization = {Apart Research},
991
+ note = {Research submission to the research sprint hosted by Apart},
992
+ howpublished = {https://apartresearch.com}
993
+ }
994
+
995
+ @misc{durmus2024steering,
996
+ author = {Esin Durmus and Alex Tamkin and Jack Clark and Jerry Wei and Jonathan Marcus and Joshua Batson and Kunal Handa and Liane Lovitt and Meg Tong and Miles McCain and Oliver Rausch and Saffron Huang and Sam Bowman and Stuart Ritchie and Tom Henighan and Deep Ganguli},
997
+ title = {Evaluating Feature Steering: A Case Study in Mitigating Social Biases},
998
+ year = {2024},
999
+ howpublished = {\url{https://anthropic.com/research/evaluating-feature-steering}},
1000
+ note = {Anthropic Research}
1001
+ }
1002
+
1003
+ @inproceedings{cao2024personalized,
1004
+ title={Personalized Steering of Large Language Models: Versatile Steering Vectors Through Bi-directional Preference Optimization},
1005
+ author={Yuanpu Cao and Tianrong Zhang and Bochuan Cao and Ziyi Yin and Lu Lin and Fenglong Ma and Jinghui Chen},
1006
+ booktitle={The Thirty-eighth Annual Conference on Neural Information Processing Systems},
1007
+ year={2024},
1008
+ url={https://openreview.net/forum?id=7qJFkuZdYo}
1009
+ }
1010
+
1011
+ @inproceedings{soo2025interpretable,
1012
+ title={Interpretable Steering of Large Language Models with Feature Guided Activation Additions},
1013
+ author={Samuel Soo and Wesley Teng and Chandrasekaran Balaganesh and Tan Guoxian and Ming YAN},
1014
+ booktitle={ICLR 2025 Workshop on Building Trust in Language Models and Applications},
1015
+ year={2025},
1016
+ url={https://openreview.net/forum?id=swRxS7s4rB}
1017
+ }
1018
+
1019
+ @misc{chalnev2024improvingsteeringvectorstargeting,
1020
+ title={Improving Steering Vectors by Targeting Sparse Autoencoder Features},
1021
+ author={Sviatoslav Chalnev and Matthew Siu and Arthur Conmy},
1022
+ year={2024},
1023
+ eprint={2411.02193},
1024
+ archivePrefix={arXiv},
1025
+ primaryClass={cs.LG},
1026
+ url={https://arxiv.org/abs/2411.02193},
1027
+ }
1028
+
1029
+ @misc{arad2025saesgoodsteering,
1030
+ title={SAEs Are Good for Steering -- If You Select the Right Features},
1031
+ author={Dana Arad and Aaron Mueller and Yonatan Belinkov},
1032
+ year={2025},
1033
+ eprint={2505.20063},
1034
+ archivePrefix={arXiv},
1035
+ primaryClass={cs.LG},
1036
+ url={https://arxiv.org/abs/2505.20063},
1037
+ }
1038
+
1039
+
1040
+ % SAE Unlearning
1041
+ @inproceedings{zhou2025llm,
1042
+ title={{LLM} Neurosurgeon: Targeted Knowledge Removal in {LLM}s using Sparse Autoencoders},
1043
+ author={Dylan Zhou and Kunal Patil and Yifan Sun and Karthik lakshmanan and Senthooran Rajamanoharan and Arthur Conmy},
1044
+ booktitle={ICLR 2025 Workshop on Building Trust in Language Models and Applications},
1045
+ year={2025},
1046
+ url={https://openreview.net/forum?id=aeQeXlG2Pw}
1047
+ }
1048
+
1049
+ @inproceedings{muhamed2025saes,
1050
+ title={{SAE}s Can Improve Unlearning: Dynamic Sparse Autoencoder Guardrails for Precision Unlearning in {LLM}s},
1051
+ author={Aashiq Muhamed and Jacopo Bonato and Mona T. Diab and Virginia Smith},
1052
+ booktitle={ICML 2025 Workshop on Reliable and Responsible Foundation Models},
1053
+ year={2025},
1054
+ url={https://openreview.net/forum?id=8gFO7ebDLT}
1055
+ }
1056
+
1057
+ @inproceedings{cywinski2025saeuron,
1058
+ title={{SA}eUron: Interpretable Concept Unlearning in Diffusion Models with Sparse Autoencoders},
1059
+ author={Bartosz Cywi{\'n}ski and Kamil Deja},
1060
+ booktitle={Forty-second International Conference on Machine Learning},
1061
+ year={2025},
1062
+ url={https://openreview.net/forum?id=6N0GxaKdX9}
1063
+ }
1064
+
1065
+ @inproceedings{wang2025model,
1066
+ title={Model Unlearning via Sparse Autoencoder Subspace Guided Projections},
1067
+ author={Xu Wang and Zihao Li and Benyou Wang and Yan Hu and Difan Zou},
1068
+ booktitle={ICML 2025 Workshop on Machine Unlearning for Generative AI},
1069
+ year={2025},
1070
+ url={https://openreview.net/forum?id=MIlqM98o9I}
1071
+ }
1072
+
1073
+
1074
+ % SAE Probing
1075
+ @article{gurnee2023finding,
1076
+ title={Finding Neurons in a Haystack: Case Studies with Sparse Probing},
1077
+ author={Wes Gurnee and Neel Nanda and Matthew Pauly and Katherine Harvey and Dmitrii Troitskii and Dimitris Bertsimas},
1078
+ journal={Transactions on Machine Learning Research},
1079
+ issn={2835-8856},
1080
+ year={2023},
1081
+ url={https://openreview.net/forum?id=JYs1R9IMJr},
1082
+ note={}
1083
+ }
1084
+
1085
+ % RL
1086
+
1087
+ @misc{schulman2017proximal,
1088
+ title = {Proximal Policy Optimization Algorithms},
1089
+ author = {Schulman, John and Wolski, Filip and Dhariwal, Prafulla and Radford, Alec and Klimov, Oleg},
1090
+ year = {2017},
1091
+ eprint = {1707.06347},
1092
+ archivePrefix= {arXiv},
1093
+ primaryClass = {cs.LG},
1094
+ url = {https://arxiv.org/abs/1707.06347},
1095
+ howpublished = {\url{https://openai.com/index/openai-baselines-ppo/}}
1096
+ }
1097
+
1098
+ @article{williams1992simple,
1099
+ title={Simple statistical gradient-following algorithms for connectionist reinforcement learning},
1100
+ author={Williams, Ronald J},
1101
+ journal={Machine Learning},
1102
+ volume={8},
1103
+ number={3-4},
1104
+ pages={229--256},
1105
+ year={1992},
1106
+ publisher={Springer}
1107
+ }
1108
+
1109
+ @article{mnih2015human,
1110
+ title={Human-level control through deep reinforcement learning},
1111
+ author={Mnih, Volodymyr and Kavukcuoglu, Koray and Silver, David and Rusu, Andrei A. and Veness, Joel and Bellemare, Marc G. and Graves, Alex and Riedmiller, Martin and Fidjeland, Andreas K. and Ostrovski, Georg and Petersen, Stig and Beattie, Charles and Sadik, Amir and Antonoglou, Ioannis and King, Helen and Kumaran, Dharshan and Wierstra, Daan and Legg, Shane and Hassabis, Demis},
1112
+ journal={Nature},
1113
+ volume={518},
1114
+ number={7540},
1115
+ pages={529--533},
1116
+ year={2015},
1117
+ publisher={Nature Publishing Group}
1118
+ }
1119
+
1120
+ @inproceedings{zhong2023a,
1121
+ title={A Theoretical Analysis of Optimistic Proximal Policy Optimization in Linear Markov Decision Processes},
1122
+ author={Han Zhong and Tong Zhang},
1123
+ booktitle={Thirty-seventh Conference on Neural Information Processing Systems},
1124
+ year={2023},
1125
+ url={https://openreview.net/forum?id=1bTG4sJ7tN}
1126
+ }
1127
+
1128
+ @article{lim2023optimality,
1129
+ title={Optimality Guarantees for Particle Belief Approximation of POMDPs},
1130
+ author={Lim, Michael and Becker, Nathan and Kochenderfer, Mykel J and Tomlin, Claire J and Sunberg, Zachary N},
1131
+ journal={Journal of Artificial Intelligence Research},
1132
+ volume={77},
1133
+ pages={1591--1636},
1134
+ year={2023},
1135
+ doi={10.1613/jair.1.14491},
1136
+ url={https://jair.org/index.php/jair/article/view/14491}
1137
+ }
1138
+
1139
+ @misc{shao2024deepseekmathpushinglimitsmathematical,
1140
+ title={DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models},
1141
+ author={Zhihong Shao and Peiyi Wang and Qihao Zhu and Runxin Xu and Junxiao Song and Xiao Bi and Haowei Zhang and Mingchuan Zhang and Y. K. Li and Y. Wu and Daya Guo},
1142
+ year={2024},
1143
+ eprint={2402.03300},
1144
+ archivePrefix={arXiv},
1145
+ primaryClass={cs.CL},
1146
+ url={https://arxiv.org/abs/2402.03300},
1147
+ }
1148
+
1149
+ @misc{deepseekai2025deepseekr1,
1150
+ title={DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning},
1151
+ author={DeepSeek-AI},
1152
+ year={2025},
1153
+ eprint={2501.12948},
1154
+ archivePrefix={arXiv},
1155
+ primaryClass={cs.CL},
1156
+ url={https://arxiv.org/abs/2501.12948},
1157
+ }
1158
+
1159
+ % RL with Residual Stream
1160
+ @inproceedings{yu-etal-2017-learning,
1161
+ title = "Learning to Skim Text",
1162
+ author = "Yu, Adams Wei and
1163
+ Lee, Hongrae and
1164
+ Le, Quoc",
1165
+ editor = "Barzilay, Regina and
1166
+ Kan, Min-Yen",
1167
+ booktitle = "Proceedings of the 55th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)",
1168
+ month = jul,
1169
+ year = "2017",
1170
+ address = "Vancouver, Canada",
1171
+ publisher = "Association for Computational Linguistics",
1172
+ url = "https://aclanthology.org/P17-1172/",
1173
+ doi = "10.18653/v1/P17-1172",
1174
+ pages = "1880--1890",
1175
+ }
1176
+
1177
+ @article{gui2019long,
1178
+ title={Long Short-Term Memory with Dynamic Skip Connections},
1179
+ volume={33},
1180
+ url={https://ojs.aaai.org/index.php/AAAI/article/view/4613},
1181
+ DOI={10.1609/aaai.v33i01.33016481},
1182
+ number={01},
1183
+ journal={Proceedings of the AAAI Conference on Artificial Intelligence},
1184
+ author={Gui, Tao and Zhang, Qi and Zhao, Lujun and Lin, Yaosong and Peng, Minlong and Gong, Jingjing and Huang, Xuanjing},
1185
+ year={2019},
1186
+ month={Jul.},
1187
+ pages={6481-6488}
1188
+ }
1189
+
1190
+ @article{xu2024dynamic,
1191
+ title = {Dynamic Layer Skipping for Large Language Models on Natural Language Understanding Tasks and Machine Translation Using Reinforcement Learning},
1192
+ author = {Xu, Wei and Jin, Xiaodong},
1193
+ journal = {Frontiers in Computing and Intelligent Systems},
1194
+ year = {2024},
1195
+ volume = {9},
1196
+ number = {3},
1197
+ doi = {10.54097/wy0g8m89},
1198
+ note = {Uses dynamic layer skipping with adapters and RL to accelerate inference of LLMs; achieves SOTA performance on NLU and MT tasks}
1199
+ }
1200
+
1201
+
1202
+ % RLVR
1203
+ @inproceedings{yue2025does,
1204
+ title={Does Reinforcement Learning Really Incentivize Reasoning Capacity in {LLM}s Beyond the Base Model?},
1205
+ author={Yang Yue and Zhiqi Chen and Rui Lu and Andrew Zhao and Zhaokai Wang and Yang Yue and Shiji Song and Gao Huang},
1206
+ booktitle={2nd AI for Math Workshop @ ICML 2025},
1207
+ year={2025},
1208
+ url={https://openreview.net/forum?id=upehLVgq1b}
1209
+ }
1210
+
1211
+ @misc{wu2025generalizationsftreinforcementlearning,
1212
+ title={On the Generalization of SFT: A Reinforcement Learning Perspective with Reward Rectification},
1213
+ author={Yongliang Wu and Yizhou Zhou and Zhou Ziheng and Yingzhe Peng and Xinyu Ye and Xinting Hu and Wenbo Zhu and Lu Qi and Ming-Hsuan Yang and Xu Yang},
1214
+ year={2025},
1215
+ eprint={2508.05629},
1216
+ archivePrefix={arXiv},
1217
+ primaryClass={cs.LG},
1218
+ url={https://arxiv.org/abs/2508.05629},
1219
+ }
1220
+
1221
+ @article{DBLP:journals/corr/abs-2505-10425,
1222
+ publtype={informal},
1223
+ author={Jingyao Wang and Wenwen Qiang and Zeen Song and Changwen Zheng and Hui Xiong},
1224
+ title={Learning to Think: Information-Theoretic Reinforcement Fine-Tuning for LLMs},
1225
+ year={2025},
1226
+ month={May},
1227
+ cdate={1746057600000},
1228
+ journal={CoRR},
1229
+ volume={abs/2505.10425},
1230
+ url={https://doi.org/10.48550/arXiv.2505.10425}
1231
+ }
1232
+
1233
+ % Token Entropy
1234
+
1235
+ @misc{wang20258020rulehighentropyminority,
1236
+ title={Beyond the 80/20 Rule: High-Entropy Minority Tokens Drive Effective Reinforcement Learning for LLM Reasoning},
1237
+ author={Shenzhi Wang and Le Yu and Chang Gao and Chujie Zheng and Shixuan Liu and Rui Lu and Kai Dang and Xionghui Chen and Jianxin Yang and Zhenru Zhang and Yuqiong Liu and An Yang and Andrew Zhao and Yang Yue and Shiji Song and Bowen Yu and Gao Huang and Junyang Lin},
1238
+ year={2025},
1239
+ eprint={2506.01939},
1240
+ archivePrefix={arXiv},
1241
+ primaryClass={cs.CL},
1242
+ url={https://arxiv.org/abs/2506.01939},
1243
+ }
1244
+
1245
+ @misc{cheng2025reasoningexplorationentropyperspective,
1246
+ title={Reasoning with Exploration: An Entropy Perspective on Reinforcement Learning for LLMs},
1247
+ author={Daixuan Cheng and Shaohan Huang and Xuekai Zhu and Bo Dai and Wayne Xin Zhao and Zhenliang Zhang and Furu Wei},
1248
+ year={2025},
1249
+ eprint={2506.14758},
1250
+ archivePrefix={arXiv},
1251
+ primaryClass={cs.CL},
1252
+ url={https://arxiv.org/abs/2506.14758},
1253
+ }
1254
+
1255
+ @misc{obeso2025realtimedetectionhallucinatedentities,
1256
+ title={Real-Time Detection of Hallucinated Entities in Long-Form Generation},
1257
+ author={Oscar Obeso and Andy Arditi and Javier Ferrando and Joshua Freeman and Cameron Holmes and Neel Nanda},
1258
+ year={2025},
1259
+ eprint={2509.03531},
1260
+ archivePrefix={arXiv},
1261
+ primaryClass={cs.CL},
1262
+ url={https://arxiv.org/abs/2509.03531},
1263
+ }
1264
+
1265
+ % Model
1266
+
1267
+ @InProceedings{pmlr-v27-baldi12a,
1268
+ title = {Autoencoders, Unsupervised Learning, and Deep Architectures},
1269
+ author = {Baldi, Pierre},
1270
+ booktitle = {Proceedings of ICML Workshop on Unsupervised and Transfer Learning},
1271
+ pages = {37--49},
1272
+ year = {2012},
1273
+ editor = {Guyon, Isabelle and Dror, Gideon and Lemaire, Vincent and Taylor, Graham and Silver, Daniel},
1274
+ volume = {27},
1275
+ series = {Proceedings of Machine Learning Research},
1276
+ address = {Bellevue, Washington, USA},
1277
+ month = {02 Jul},
1278
+ publisher = {PMLR},
1279
+ pdf = {http://proceedings.mlr.press/v27/baldi12a/baldi12a.pdf},
1280
+ url = {https://proceedings.mlr.press/v27/baldi12a.html},
1281
+ abstract = {Autoencoders play a fundamental role in unsupervised learning and in deep architectures for transfer learning and other tasks. In spite of their fundamental role, only linear autoencoders over the real numbers have been solved analytically. Here we present a general mathematical framework for the study of both linear and non-linear autoencoders. The framework allows one to derive an analytical treatment for the most non-linear autoencoder, the Boolean autoencoder. Learning in the Boolean autoencoder is equivalent to a clustering problem that can be solved in polynomial time when the number of clusters is small and becomes NP complete when the number of clusters is large. The framework sheds light on the different kinds of autoencoders, their learning complexity, their horizontal and vertical composability in deep architectures, their critical points, and their fundamental connections to clustering, Hebbian learning, and information theory.}
1282
+ }
1283
+
1284
+ @misc{grattafiori2024llama3herdmodels,
1285
+ title={The Llama 3 Herd of Models},
1286
+ author={Llama Team and Aaron Grattafiori and Abhimanyu Dubey and Abhinav Jauhri and Abhinav Pandey and Abhishek Kadian and Ahmad Al-Dahle and Aiesha Letman and Akhil Mathur and Alan Schelten and Alex Vaughan and Amy Yang and Angela Fan and Anirudh Goyal and Anthony Hartshorn and Aobo Yang and Archi Mitra and Archie Sravankumar and Artem Korenev and Arthur Hinsvark and Arun Rao and Aston Zhang and Aurelien Rodriguez and Austen Gregerson and Ava Spataru and Baptiste Roziere and Bethany Biron and Binh Tang and Bobbie Chern and Charlotte Caucheteux and Chaya Nayak and Chloe Bi and Chris Marra and Chris McConnell and Christian Keller and Christophe Touret and Chunyang Wu and Corinne Wong and Cristian Canton Ferrer and Cyrus Nikolaidis and Damien Allonsius and Daniel Song and Danielle Pintz and Danny Livshits and Danny Wyatt and David Esiobu and Dhruv Choudhary and Dhruv Mahajan and Diego Garcia-Olano and Diego Perino and Dieuwke Hupkes and Egor Lakomkin and Ehab AlBadawy and Elina Lobanova and Emily Dinan and Eric Michael Smith and Filip Radenovic and Francisco GuzmΓ‘n and Frank Zhang and Gabriel Synnaeve and Gabrielle Lee and Georgia Lewis Anderson and Govind Thattai and Graeme Nail and Gregoire Mialon and Guan Pang and Guillem Cucurell and Hailey Nguyen and Hannah Korevaar and Hu Xu and Hugo Touvron and Iliyan Zarov and Imanol Arrieta Ibarra and Isabel Kloumann and Ishan Misra and Ivan Evtimov and Jack Zhang and Jade Copet and Jaewon Lee and Jan Geffert and Jana Vranes and Jason Park and Jay Mahadeokar and Jeet Shah and Jelmer van der Linde and Jennifer Billock and Jenny Hong and Jenya Lee and Jeremy Fu and Jianfeng Chi and Jianyu Huang and Jiawen Liu and Jie Wang and Jiecao Yu and Joanna Bitton and Joe Spisak and Jongsoo Park and Joseph Rocca and Joshua Johnstun and Joshua Saxe and Junteng Jia and Kalyan Vasuden Alwala and Karthik Prasad and Kartikeya Upasani and Kate Plawiak and Ke Li and Kenneth Heafield and Kevin Stone and Khalid El-Arini and Krithika Iyer and Kshitiz Malik and Kuenley Chiu and Kunal Bhalla and Kushal Lakhotia and Lauren Rantala-Yeary and Laurens van der Maaten and Lawrence Chen and Liang Tan and Liz Jenkins and Louis Martin and Lovish Madaan and Lubo Malo and Lukas Blecher and Lukas Landzaat and Luke de Oliveira and Madeline Muzzi and Mahesh Pasupuleti and Mannat Singh and Manohar Paluri and Marcin Kardas and Maria Tsimpoukelli and Mathew Oldham and Mathieu Rita and Maya Pavlova and Melanie Kambadur and Mike Lewis and Min Si and Mitesh Kumar Singh and Mona Hassan and Naman Goyal and Narjes Torabi and Nikolay Bashlykov and Nikolay Bogoychev and Niladri Chatterji and Ning Zhang and Olivier Duchenne and Onur Γ‡elebi and Patrick Alrassy and Pengchuan Zhang and Pengwei Li and Petar Vasic and Peter Weng and Prajjwal Bhargava and Pratik Dubal and Praveen Krishnan and Punit Singh Koura and Puxin Xu and Qing He and Qingxiao Dong and Ragavan Srinivasan and Raj Ganapathy and Ramon Calderer and Ricardo Silveira Cabral and Robert Stojnic and Roberta Raileanu and Rohan Maheswari and Rohit Girdhar and Rohit Patel and Romain Sauvestre and Ronnie Polidoro and Roshan Sumbaly and Ross Taylor and Ruan Silva and Rui Hou and Rui Wang and Saghar Hosseini and Sahana Chennabasappa and Sanjay Singh and Sean Bell and Seohyun Sonia Kim and Sergey Edunov and Shaoliang Nie and Sharan Narang and Sharath Raparthy and Sheng Shen and Shengye Wan and Shruti Bhosale and Shun Zhang and Simon Vandenhende and Soumya Batra and Spencer Whitman and Sten Sootla and Stephane Collot and Suchin Gururangan and Sydney Borodinsky and Tamar Herman and Tara Fowler and Tarek Sheasha and Thomas Georgiou and Thomas Scialom and Tobias Speckbacher and Todor Mihaylov and Tong Xiao and Ujjwal Karn and Vedanuj Goswami and Vibhor Gupta and Vignesh Ramanathan and Viktor Kerkez and Vincent Gonguet and Virginie Do and Vish Vogeti and VΓ­tor Albiero and Vladan Petrovic and Weiwei Chu and Wenhan Xiong and Wenyin Fu and Whitney Meers and Xavier Martinet and Xiaodong Wang and Xiaofang Wang and Xiaoqing Ellen Tan and Xide Xia and Xinfeng Xie and Xuchao Jia and Xuewei Wang and Yaelle Goldschlag and Yashesh Gaur and Yasmine Babaei and Yi Wen and Yiwen Song and Yuchen Zhang and Yue Li and Yuning Mao and Zacharie Delpierre Coudert and Zheng Yan and Zhengxing Chen and Zoe Papakipos and Aaditya Singh and Aayushi Srivastava and Abha Jain and Adam Kelsey and Adam Shajnfeld and Adithya Gangidi and Adolfo Victoria and Ahuva Goldstand and Ajay Menon and Ajay Sharma and Alex Boesenberg and Alexei Baevski and Allie Feinstein and Amanda Kallet and Amit Sangani and Amos Teo and Anam Yunus and Andrei Lupu and Andres Alvarado and Andrew Caples and Andrew Gu and Andrew Ho and Andrew Poulton and Andrew Ryan and Ankit Ramchandani and Annie Dong and Annie Franco and Anuj Goyal and Aparajita Saraf and Arkabandhu Chowdhury and Ashley Gabriel and Ashwin Bharambe and Assaf Eisenman and Azadeh Yazdan and Beau James and Ben Maurer and Benjamin Leonhardi and Bernie Huang and Beth Loyd and Beto De Paola and Bhargavi Paranjape and Bing Liu and Bo Wu and Boyu Ni and Braden Hancock and Bram Wasti and Brandon Spence and Brani Stojkovic and Brian Gamido and Britt Montalvo and Carl Parker and Carly Burton and Catalina Mejia and Ce Liu and Changhan Wang and Changkyu Kim and Chao Zhou and Chester Hu and Ching-Hsiang Chu and Chris Cai and Chris Tindal and Christoph Feichtenhofer and Cynthia Gao and Damon Civin and Dana Beaty and Daniel Kreymer and Daniel Li and David Adkins and David Xu and Davide Testuggine and Delia David and Devi Parikh and Diana Liskovich and Didem Foss and Dingkang Wang and Duc Le and Dustin Holland and Edward Dowling and Eissa Jamil and Elaine Montgomery and Eleonora Presani and Emily Hahn and Emily Wood and Eric-Tuan Le and Erik Brinkman and Esteban Arcaute and Evan Dunbar and Evan Smothers and Fei Sun and Felix Kreuk and Feng Tian and Filippos Kokkinos and Firat Ozgenel and Francesco Caggioni and Frank Kanayet and Frank Seide and Gabriela Medina Florez and Gabriella Schwarz and Gada Badeer and Georgia Swee and Gil Halpern and Grant Herman and Grigory Sizov and Guangyi and Zhang and Guna Lakshminarayanan and Hakan Inan and Hamid Shojanazeri and Han Zou and Hannah Wang and Hanwen Zha and Haroun Habeeb and Harrison Rudolph and Helen Suk and Henry Aspegren and Hunter Goldman and Hongyuan Zhan and Ibrahim Damlaj and Igor Molybog and Igor Tufanov and Ilias Leontiadis and Irina-Elena Veliche and Itai Gat and Jake Weissman and James Geboski and James Kohli and Janice Lam and Japhet Asher and Jean-Baptiste Gaya and Jeff Marcus and Jeff Tang and Jennifer Chan and Jenny Zhen and Jeremy Reizenstein and Jeremy Teboul and Jessica Zhong and Jian Jin and Jingyi Yang and Joe Cummings and Jon Carvill and Jon Shepard and Jonathan McPhie and Jonathan Torres and Josh Ginsburg and Junjie Wang and Kai Wu and Kam Hou U and Karan Saxena and Kartikay Khandelwal and Katayoun Zand and Kathy Matosich and Kaushik Veeraraghavan and Kelly Michelena and Keqian Li and Kiran Jagadeesh and Kun Huang and Kunal Chawla and Kyle Huang and Lailin Chen and Lakshya Garg and Lavender A and Leandro Silva and Lee Bell and Lei Zhang and Liangpeng Guo and Licheng Yu and Liron Moshkovich and Luca Wehrstedt and Madian Khabsa and Manav Avalani and Manish Bhatt and Martynas Mankus and Matan Hasson and Matthew Lennie and Matthias Reso and Maxim Groshev and Maxim Naumov and Maya Lathi and Meghan Keneally and Miao Liu and Michael L. Seltzer and Michal Valko and Michelle Restrepo and Mihir Patel and Mik Vyatskov and Mikayel Samvelyan and Mike Clark and Mike Macey and Mike Wang and Miquel Jubert Hermoso and Mo Metanat and Mohammad Rastegari and Munish Bansal and Nandhini Santhanam and Natascha Parks and Natasha White and Navyata Bawa and Nayan Singhal and Nick Egebo and Nicolas Usunier and Nikhil Mehta and Nikolay Pavlovich Laptev and Ning Dong and Norman Cheng and Oleg Chernoguz and Olivia Hart and Omkar Salpekar and Ozlem Kalinli and Parkin Kent and Parth Parekh and Paul Saab and Pavan Balaji and Pedro Rittner and Philip Bontrager and Pierre Roux and Piotr Dollar and Polina Zvyagina and Prashant Ratanchandani and Pritish Yuvraj and Qian Liang and Rachad Alao and Rachel Rodriguez and Rafi Ayub and Raghotham Murthy and Raghu Nayani and Rahul Mitra and Rangaprabhu Parthasarathy and Raymond Li and Rebekkah Hogan and Robin Battey and Rocky Wang and Russ Howes and Ruty Rinott and Sachin Mehta and Sachin Siby and Sai Jayesh Bondu and Samyak Datta and Sara Chugh and Sara Hunt and Sargun Dhillon and Sasha Sidorov and Satadru Pan and Saurabh Mahajan and Saurabh Verma and Seiji Yamamoto and Sharadh Ramaswamy and Shaun Lindsay and Shaun Lindsay and Sheng Feng and Shenghao Lin and Shengxin Cindy Zha and Shishir Patil and Shiva Shankar and Shuqiang Zhang and Shuqiang Zhang and Sinong Wang and Sneha Agarwal and Soji Sajuyigbe and Soumith Chintala and Stephanie Max and Stephen Chen and Steve Kehoe and Steve Satterfield and Sudarshan Govindaprasad and Sumit Gupta and Summer Deng and Sungmin Cho and Sunny Virk and Suraj Subramanian and Sy Choudhury and Sydney Goldman and Tal Remez and Tamar Glaser and Tamara Best and Thilo Koehler and Thomas Robinson and Tianhe Li and Tianjun Zhang and Tim Matthews and Timothy Chou and Tzook Shaked and Varun Vontimitta and Victoria Ajayi and Victoria Montanez and Vijai Mohan and Vinay Satish Kumar and Vishal Mangla and Vlad Ionescu and Vlad Poenaru and Vlad Tiberiu Mihailescu and Vladimir Ivanov and Wei Li and Wenchen Wang and Wenwen Jiang and Wes Bouaziz and Will Constable and Xiaocheng Tang and Xiaojian Wu and Xiaolan Wang and Xilun Wu and Xinbo Gao and Yaniv Kleinman and Yanjun Chen and Ye Hu and Ye Jia and Ye Qi and Yenda Li and Yilin Zhang and Ying Zhang and Yossi Adi and Youngjin Nam and Yu and Wang and Yu Zhao and Yuchen Hao and Yundi Qian and Yunlu Li and Yuzi He and Zach Rait and Zachary DeVito and Zef Rosnbrick and Zhaoduo Wen and Zhenyu Yang and Zhiwei Zhao and Zhiyu Ma},
1287
+ year={2024},
1288
+ eprint={2407.21783},
1289
+ archivePrefix={arXiv},
1290
+ primaryClass={cs.AI},
1291
+ url={https://arxiv.org/abs/2407.21783},
1292
+ }
1293
+
1294
+ @misc{bloom2024gpt2residualsaes,
1295
+ title = {Open Source Sparse Autoencoders for all Residual Stream Layers of GPT2 Small},
1296
+ author = {Joseph Bloom},
1297
+ year = {2024},
1298
+ url = {https://www.alignmentforum.org/posts/f9EgfLSurAiqRJySD/open-source-sparse-autoencoders-for-all-residual-stream},
1299
+ }
1300
+
1301
+ @misc{gemmateam2024gemma2improvingopen,
1302
+ title={Gemma 2: Improving Open Language Models at a Practical Size},
1303
+ author={Gemma Team and Morgane Riviere and Shreya Pathak and Pier Giuseppe Sessa and Cassidy Hardin and Surya Bhupatiraju and LΓ©onard Hussenot and Thomas Mesnard and Bobak Shahriari and Alexandre RamΓ© and Johan Ferret and Peter Liu and Pouya Tafti and Abe Friesen and Michelle Casbon and Sabela Ramos and Ravin Kumar and Charline Le Lan and Sammy Jerome and Anton Tsitsulin and Nino Vieillard and Piotr Stanczyk and Sertan Girgin and Nikola Momchev and Matt Hoffman and Shantanu Thakoor and Jean-Bastien Grill and Behnam Neyshabur and Olivier Bachem and Alanna Walton and Aliaksei Severyn and Alicia Parrish and Aliya Ahmad and Allen Hutchison and Alvin Abdagic and Amanda Carl and Amy Shen and Andy Brock and Andy Coenen and Anthony Laforge and Antonia Paterson and Ben Bastian and Bilal Piot and Bo Wu and Brandon Royal and Charlie Chen and Chintu Kumar and Chris Perry and Chris Welty and Christopher A. Choquette-Choo and Danila Sinopalnikov and David Weinberger and Dimple Vijaykumar and Dominika RogoziΕ„ska and Dustin Herbison and Elisa Bandy and Emma Wang and Eric Noland and Erica Moreira and Evan Senter and Evgenii Eltyshev and Francesco Visin and Gabriel Rasskin and Gary Wei and Glenn Cameron and Gus Martins and Hadi Hashemi and Hanna Klimczak-PluciΕ„ska and Harleen Batra and Harsh Dhand and Ivan Nardini and Jacinda Mein and Jack Zhou and James Svensson and Jeff Stanway and Jetha Chan and Jin Peng Zhou and Joana Carrasqueira and Joana Iljazi and Jocelyn Becker and Joe Fernandez and Joost van Amersfoort and Josh Gordon and Josh Lipschultz and Josh Newlan and Ju-yeong Ji and Kareem Mohamed and Kartikeya Badola and Kat Black and Katie Millican and Keelin McDonell and Kelvin Nguyen and Kiranbir Sodhia and Kish Greene and Lars Lowe Sjoesund and Lauren Usui and Laurent Sifre and Lena Heuermann and Leticia Lago and Lilly McNealus and Livio Baldini Soares and Logan Kilpatrick and Lucas Dixon and Luciano Martins and Machel Reid and Manvinder Singh and Mark Iverson and Martin GΓΆrner and Mat Velloso and Mateo Wirth and Matt Davidow and Matt Miller and Matthew Rahtz and Matthew Watson and Meg Risdal and Mehran Kazemi and Michael Moynihan and Ming Zhang and Minsuk Kahng and Minwoo Park and Mofi Rahman and Mohit Khatwani and Natalie Dao and Nenshad Bardoliwalla and Nesh Devanathan and Neta Dumai and Nilay Chauhan and Oscar Wahltinez and Pankil Botarda and Parker Barnes and Paul Barham and Paul Michel and Pengchong Jin and Petko Georgiev and Phil Culliton and Pradeep Kuppala and Ramona Comanescu and Ramona Merhej and Reena Jana and Reza Ardeshir Rokni and Rishabh Agarwal and Ryan Mullins and Samaneh Saadat and Sara Mc Carthy and Sarah Cogan and Sarah Perrin and SΓ©bastien M. R. Arnold and Sebastian Krause and Shengyang Dai and Shruti Garg and Shruti Sheth and Sue Ronstrom and Susan Chan and Timothy Jordan and Ting Yu and Tom Eccles and Tom Hennigan and Tomas Kocisky and Tulsee Doshi and Vihan Jain and Vikas Yadav and Vilobh Meshram and Vishal Dharmadhikari and Warren Barkley and Wei Wei and Wenming Ye and Woohyun Han and Woosuk Kwon and Xiang Xu and Zhe Shen and Zhitao Gong and Zichuan Wei and Victor Cotruta and Phoebe Kirk and Anand Rao and Minh Giang and Ludovic Peran and Tris Warkentin and Eli Collins and Joelle Barral and Zoubin Ghahramani and Raia Hadsell and D. Sculley and Jeanine Banks and Anca Dragan and Slav Petrov and Oriol Vinyals and Jeff Dean and Demis Hassabis and Koray Kavukcuoglu and Clement Farabet and Elena Buchatskaya and Sebastian Borgeaud and Noah Fiedel and Armand Joulin and Kathleen Kenealy and Robert Dadashi and Alek Andreev},
1304
+ year={2024},
1305
+ eprint={2408.00118},
1306
+ archivePrefix={arXiv},
1307
+ primaryClass={cs.CL},
1308
+ url={https://arxiv.org/abs/2408.00118},
1309
+ }
1310
+
1311
+ @article{radford2019language,
1312
+ author = {Radford, Alec and Wu, Jeffrey and Child, Rewon and Luan, David and Amodei, Dario and Sutskever, Ilya},
1313
+ interhash = {b926ece39c03cdf5499f6540cf63babd},
1314
+ intrahash = {33e4b003b64b1060334660fbf6db1f3f},
1315
+ journal = {OpenAI},
1316
+ keywords = {gpt gpt2 languagemodelling transferlearning transformer},
1317
+ note = {Accessed: 2024-11-15},
1318
+ timestamp = {2024-11-15T12:44:17.000+0100},
1319
+ title = {Language Models are Unsupervised Multitask Learners},
1320
+ url = {https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf},
1321
+ year = 2019
1322
+ }
1323
+
1324
+ % Dataset
1325
+ @inproceedings{hendrycks2021measuring,
1326
+ title={Measuring Massive Multitask Language Understanding},
1327
+ author={Dan Hendrycks and Collin Burns and Steven Basart and Andy Zou and Mantas Mazeika and Dawn Song and Jacob Steinhardt},
1328
+ booktitle={International Conference on Learning Representations},
1329
+ year={2021},
1330
+ url={https://openreview.net/forum?id=d7KBjmI3GmQ}
1331
+ }
1332
+
1333
+
1334
+
1335
+ % Benchmark
1336
+ @inproceedings{parrish-etal-2022-bbq,
1337
+ title = "{BBQ}: A hand-built bias benchmark for question answering",
1338
+ author = "Parrish, Alicia and
1339
+ Chen, Angelica and
1340
+ Nangia, Nikita and
1341
+ Padmakumar, Vishakh and
1342
+ Phang, Jason and
1343
+ Thompson, Jana and
1344
+ Htut, Phu Mon and
1345
+ Bowman, Samuel",
1346
+ editor = "Muresan, Smaranda and
1347
+ Nakov, Preslav and
1348
+ Villavicencio, Aline",
1349
+ booktitle = "Findings of the Association for Computational Linguistics: ACL 2022",
1350
+ month = may,
1351
+ year = "2022",
1352
+ address = "Dublin, Ireland",
1353
+ publisher = "Association for Computational Linguistics",
1354
+ url = "https://aclanthology.org/2022.findings-acl.165/",
1355
+ doi = "10.18653/v1/2022.findings-acl.165",
1356
+ pages = "2086--2105"
1357
+ }
1358
+
1359
+ @misc{wei2024measuringshortformfactualitylarge,
1360
+ title={Measuring short-form factuality in large language models},
1361
+ author={Jason Wei and Nguyen Karina and Hyung Won Chung and Yunxin Joy Jiao and Spencer Papay and Amelia Glaese and John Schulman and William Fedus},
1362
+ year={2024},
1363
+ eprint={2411.04368},
1364
+ archivePrefix={arXiv},
1365
+ primaryClass={cs.CL},
1366
+ url={https://openai.com/index/introducing-simpleqa/},
1367
+ }
1368
+
1369
+ @inproceedings{mazeika2024harmbench,
1370
+ title={HarmBench: A Standardized Evaluation Framework for Automated Red Teaming and Robust Refusal},
1371
+ author={Mantas Mazeika and Long Phan and Xuwang Yin and Andy Zou and Zifan Wang and Norman Mu and Elham Sakhaee and Nathaniel Li and Steven Basart and Bo Li and David Forsyth and Dan Hendrycks},
1372
+ booktitle={Forty-first International Conference on Machine Learning},
1373
+ year={2024},
1374
+ url={https://openreview.net/forum?id=f3TUipYU3U}
1375
+ }
1376
+
1377
+ @inproceedings{rottger-etal-2024-xstest,
1378
+ title = "{XST}est: A Test Suite for Identifying Exaggerated Safety Behaviours in Large Language Models",
1379
+ author = {R{\"o}ttger, Paul and
1380
+ Kirk, Hannah and
1381
+ Vidgen, Bertie and
1382
+ Attanasio, Giuseppe and
1383
+ Bianchi, Federico and
1384
+ Hovy, Dirk},
1385
+ editor = "Duh, Kevin and
1386
+ Gomez, Helena and
1387
+ Bethard, Steven",
1388
+ booktitle = "Proceedings of the 2024 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies (Volume 1: Long Papers)",
1389
+ month = jun,
1390
+ year = "2024",
1391
+ address = "Mexico City, Mexico",
1392
+ publisher = "Association for Computational Linguistics",
1393
+ url = "https://aclanthology.org/2024.naacl-long.301/",
1394
+ doi = "10.18653/v1/2024.naacl-long.301",
1395
+ pages = "5377--5400"
1396
+ }
1397
+
1398
+ @inproceedings{li2024the,
1399
+ title={The {WMDP} Benchmark: Measuring and Reducing Malicious Use with Unlearning},
1400
+ author={Nathaniel Li and Alexander Pan and Anjali Gopal and Summer Yue and Daniel Berrios and Alice Gatti and Justin D. Li and Ann-Kathrin Dombrowski and Shashwat Goel and Gabriel Mukobi and Nathan Helm-Burger and Rassin Lababidi and Lennart Justen and Andrew Bo Liu and Michael Chen and Isabelle Barrass and Oliver Zhang and Xiaoyuan Zhu and Rishub Tamirisa and Bhrugu Bharathi and Ariel Herbert-Voss and Cort B Breuer and Andy Zou and Mantas Mazeika and Zifan Wang and Palash Oswal and Weiran Lin and Adam Alfred Hunt and Justin Tienken-Harder and Kevin Y. Shih and Kemper Talley and John Guan and Ian Steneker and David Campbell and Brad Jokubaitis and Steven Basart and Stephen Fitz and Ponnurangam Kumaraguru and Kallol Krishna Karmakar and Uday Tupakula and Vijay Varadharajan and Yan Shoshitaishvili and Jimmy Ba and Kevin M. Esvelt and Alexandr Wang and Dan Hendrycks},
1401
+ booktitle={Forty-first International Conference on Machine Learning},
1402
+ year={2024},
1403
+ url={https://openreview.net/forum?id=xlr6AUDuJz}
1404
+ }
1405
+
1406
+ @inproceedings{king2024hearts,
1407
+ title={{HEARTS}: A Holistic Framework for Explainable, Sustainable and Robust Text Stereotype Detection},
1408
+ author={Theo King and Zekun Wu and Adriano Koshiyama and Emre Kazim and Philip Colin Treleaven},
1409
+ booktitle={Neurips Safe Generative AI Workshop 2024},
1410
+ year={2024},
1411
+ url={https://openreview.net/forum?id=arh91riKiQ}
1412
+ }
1413
+
1414
+ @inproceedings{wang2024mmlupro,
1415
+ title={{MMLU}-Pro: A More Robust and Challenging Multi-Task Language Understanding Benchmark},
1416
+ author={Yubo Wang and Xueguang Ma and Ge Zhang and Yuansheng Ni and Abhranil Chandra and Shiguang Guo and Weiming Ren and Aaran Arulraj and Xuan He and Ziyan Jiang and Tianle Li and Max Ku and Kai Wang and Alex Zhuang and Rongqi Fan and Xiang Yue and Wenhu Chen},
1417
+ booktitle={The Thirty-eight Conference on Neural Information Processing Systems Datasets and Benchmarks Track},
1418
+ year={2024},
1419
+ url={https://openreview.net/forum?id=y10DM6R2r3}
1420
+ }
1421
+
1422
+ @misc{cobbe2021trainingverifierssolvemath,
1423
+ title={Training Verifiers to Solve Math Word Problems},
1424
+ author={Karl Cobbe and Vineet Kosaraju and Mohammad Bavarian and Mark Chen and Heewoo Jun and Lukasz Kaiser and Matthias Plappert and Jerry Tworek and Jacob Hilton and Reiichiro Nakano and Christopher Hesse and John Schulman},
1425
+ year={2021},
1426
+ eprint={2110.14168},
1427
+ archivePrefix={arXiv},
1428
+ primaryClass={cs.LG},
1429
+ url={https://arxiv.org/abs/2110.14168},
1430
+ }
1431
+
1432
+ % Brain
1433
+ @article{Pang2023,
1434
+ author = {James C. Pang and Kevin M. Aquino and Marianne Oldehinkel and Peter A. Robinson and Ben D. Fulcher and Michael Breakspear and Alex Fornito},
1435
+ title = {Geometric constraints on human brain function},
1436
+ journal = {Nature},
1437
+ volume = {618},
1438
+ number = {7965},
1439
+ pages = {566--574},
1440
+ year = {2023},
1441
+ month = {June},
1442
+ doi = {10.1038/s41586-023-06098-1},
1443
+ url = {https://doi.org/10.1038/s41586-023-06098-1},
1444
+ issn = {1476-4687}
1445
+ }
1446
+
1447
+ @article{Poo2009,
1448
+ author = {Cindy Poo and Jeffry S. Isaacson},
1449
+ title = {Odor representations in olfactory cortex: "sparse" coding, global inhibition, and oscillations},
1450
+ journal = {Neuron},
1451
+ volume = {62},
1452
+ number = {6},
1453
+ pages = {850--861},
1454
+ year = {2009},
1455
+ month = {June},
1456
+ doi = {10.1016/j.neuron.2009.05.022},
1457
+ issn = {0896-6273},
1458
+ pmid = {19555653},
1459
+ pmcid = {PMC2702531},
1460
+ url = {https://doi.org/10.1016/j.neuron.2009.05.022},
1461
+ note = {Supported by NIH grants: F31 DC009366, R01 DC004682, T32 NS061847}
1462
+ }
1463
+
1464
+ @book{James2007,
1465
+ author = {William James},
1466
+ title = {The Principles of Psychology, Vol. 1},
1467
+ publisher = {Cosimo, Inc.},
1468
+ year = {2007}
1469
+ }
1470
+
1471
+ @article{Coppola2022,
1472
+ author = {Peter Coppola and Judith Allanson and Lorina Naci and Ram Adapa and Paola Finoia and Guy B. Williams and John D. Pickard and Adrian M. Owen and David K. Menon and Emmanuel A. Stamatakis},
1473
+ title = {The complexity of the stream of consciousness},
1474
+ journal = {Communications Biology},
1475
+ year = {2022},
1476
+ volume = {5},
1477
+ number = {1},
1478
+ pages = {1173},
1479
+ doi = {10.1038/s42003-022-04109-x},
1480
+ url = {https://doi.org/10.1038/s42003-022-04109-x},
1481
+ issn = {2399-3642}
1482
+ }
1483
+
1484
+ @article{DelPozo2021,
1485
+ author = {S. M. Del Pozo and H. Laufs and V. Bonhomme and S. Laureys and P. Balenzuela and E. Tagliazucchi},
1486
+ title = {Unconsciousness reconfigures modular brain network dynamics},
1487
+ journal = {Chaos},
1488
+ year = {2021},
1489
+ volume = {31},
1490
+ number = {9},
1491
+ pages = {093117},
1492
+ doi = {10.1063/5.0046047},
1493
+ issn = {1054-1500},
1494
+ pmid = {34598477}
1495
+ }
1496
+
1497
+ % AI Alignment
1498
+ @inproceedings{qi2025safety,
1499
+ title={Safety Alignment Should be Made More Than Just a Few Tokens Deep},
1500
+ author={Xiangyu Qi and Ashwinee Panda and Kaifeng Lyu and Xiao Ma and Subhrajit Roy and Ahmad Beirami and Prateek Mittal and Peter Henderson},
1501
+ booktitle={The Thirteenth International Conference on Learning Representations},
1502
+ year={2025},
1503
+ url={https://openreview.net/forum?id=6Mxhg9PtDE}
1504
+ }
1505
+
1506
+
1507
+ % Emergent Misalignment
1508
+ @inproceedings{qi2024fine,
1509
+ author={Xiangyu Qi and Yi Zeng and Tinghao Xie and Pin-Yu Chen and Ruoxi Jia and Prateek Mittal and Peter Henderson},
1510
+ title={Fine-tuning Aligned Language Models Compromises Safety, Even When Users Do Not Intend To!},
1511
+ year={2024},
1512
+ cdate={1704067200000},
1513
+ url={https://openreview.net/forum?id=hTEGyKf0dZ},
1514
+ booktitle={ICLR}
1515
+ }
1516
+
1517
+ @inproceedings{betley2025emergent,
1518
+ title={Emergent Misalignment: Narrow finetuning can produce broadly misaligned {LLM}s},
1519
+ author={Jan Betley and Daniel Chee Hian Tan and Niels Warncke and Anna Sztyber-Betley and Xuchan Bao and Mart{\'\i}n Soto and Nathan Labenz and Owain Evans},
1520
+ booktitle={Forty-second International Conference on Machine Learning},
1521
+ year={2025},
1522
+ url={https://openreview.net/forum?id=aOIJ2gVRWW}
1523
+ }
1524
+ @inproceedings{casademunt2025steering,
1525
+ title={Steering Fine-Tuning Generalization with Targeted Concept Ablation},
1526
+ author={Helena Casademunt and Caden Juang and Samuel Marks and Senthooran Rajamanoharan and Neel Nanda},
1527
+ booktitle={ICLR 2025 Workshop on Building Trust in Language Models and Applications},
1528
+ year={2025},
1529
+ url={https://openreview.net/forum?id=2HyKWpAB4i}
1530
+ }
1531
+
1532
+ @inproceedings{ji-etal-2025-language-models,
1533
+ title = "Language Models Resist Alignment: Evidence From Data Compression",
1534
+ author = "Ji, Jiaming and
1535
+ Wang, Kaile and
1536
+ Qiu, Tianyi Alex and
1537
+ Chen, Boyuan and
1538
+ Zhou, Jiayi and
1539
+ Li, Changye and
1540
+ Lou, Hantao and
1541
+ Dai, Josef and
1542
+ Liu, Yunhuai and
1543
+ Yang, Yaodong",
1544
+ editor = "Che, Wanxiang and
1545
+ Nabende, Joyce and
1546
+ Shutova, Ekaterina and
1547
+ Pilehvar, Mohammad Taher",
1548
+ booktitle = "Proceedings of the 63rd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)",
1549
+ month = jul,
1550
+ year = "2025",
1551
+ address = "Vienna, Austria",
1552
+ publisher = "Association for Computational Linguistics",
1553
+ url = "https://aclanthology.org/2025.acl-long.1141/",
1554
+ doi = "10.18653/v1/2025.acl-long.1141",
1555
+ pages = "23411--23432",
1556
+ ISBN = "979-8-89176-251-0"
1557
+ }
1558
+
1559
+ @misc{guan2025deliberativealignmentreasoningenables,
1560
+ title={Deliberative Alignment: Reasoning Enables Safer Language Models},
1561
+ author={Melody Y. Guan and Manas Joglekar and Eric Wallace and Saachi Jain and Boaz Barak and Alec Helyar and Rachel Dias and Andrea Vallone and Hongyu Ren and Jason Wei and Hyung Won Chung and Sam Toyer and Johannes Heidecke and Alex Beutel and Amelia Glaese},
1562
+ year={2024},
1563
+ howpublished={\url{https://openai.com/index/deliberative-alignment/}},
1564
+ }
1565
+
1566
+ @inproceedings{rawte-etal-2023-troubling,
1567
+ title = "The Troubling Emergence of Hallucination in Large Language Models - An Extensive Definition, Quantification, and Prescriptive Remediations",
1568
+ author = "Rawte, Vipula and
1569
+ Chakraborty, Swagata and
1570
+ Pathak, Agnibh and
1571
+ Sarkar, Anubhav and
1572
+ Tonmoy, S.M Towhidul Islam and
1573
+ Chadha, Aman and
1574
+ Sheth, Amit and
1575
+ Das, Amitava",
1576
+ editor = "Bouamor, Houda and
1577
+ Pino, Juan and
1578
+ Bali, Kalika",
1579
+ booktitle = "Proceedings of the 2023 Conference on Empirical Methods in Natural Language Processing",
1580
+ month = dec,
1581
+ year = "2023",
1582
+ address = "Singapore",
1583
+ publisher = "Association for Computational Linguistics",
1584
+ url = "https://aclanthology.org/2023.emnlp-main.155/",
1585
+ doi = "10.18653/v1/2023.emnlp-main.155",
1586
+ pages = "2541--2573"
1587
+ }
1588
+
1589
+ % Memory
1590
+ @inproceedings{li-etal-2023-large,
1591
+ title = "Large Language Models with Controllable Working Memory",
1592
+ author = "Li, Daliang and
1593
+ Rawat, Ankit Singh and
1594
+ Zaheer, Manzil and
1595
+ Wang, Xin and
1596
+ Lukasik, Michal and
1597
+ Veit, Andreas and
1598
+ Yu, Felix and
1599
+ Kumar, Sanjiv",
1600
+ editor = "Rogers, Anna and
1601
+ Boyd-Graber, Jordan and
1602
+ Okazaki, Naoaki",
1603
+ booktitle = "Findings of the Association for Computational Linguistics: ACL 2023",
1604
+ month = jul,
1605
+ year = "2023",
1606
+ address = "Toronto, Canada",
1607
+ publisher = "Association for Computational Linguistics",
1608
+ url = "https://aclanthology.org/2023.findings-acl.112/",
1609
+ doi = "10.18653/v1/2023.findings-acl.112",
1610
+ pages = "1774--1793"
1611
+ }
1612
+
1613
+ @inproceedings{fountas2025humaninspired,
1614
+ title={Human-inspired Episodic Memory for Infinite Context {LLM}s},
1615
+ author={Zafeirios Fountas and Martin Benfeghoul and Adnan Oomerjee and Fenia Christopoulou and Gerasimos Lampouras and Haitham Bou Ammar and Jun Wang},
1616
+ booktitle={The Thirteenth International Conference on Learning Representations},
1617
+ year={2025},
1618
+ url={https://openreview.net/forum?id=BI2int5SAC}
1619
+ }
1620
+
1621
+
1622
+ @article{Tikochinski2025,
1623
+ author={Tikochinski, Refael
1624
+ and Goldstein, Ariel
1625
+ and Meiri, Yoav
1626
+ and Hasson, Uri
1627
+ and Reichart, Roi},
1628
+ title={Incremental accumulation of linguistic context in artificial and biological neural networks},
1629
+ journal={Nature Communications},
1630
+ year={2025},
1631
+ month={Jan},
1632
+ day={18},
1633
+ volume={16},
1634
+ number={1},
1635
+ pages={803},
1636
+ issn={2041-1723},
1637
+ doi={10.1038/s41467-025-56162-9},
1638
+ url={https://doi.org/10.1038/s41467-025-56162-9}
1639
+ }
1640
+
1641
+
1642
+ % Layer sharing
1643
+ @inproceedings{elhoushi2024,
1644
+ author={Mostafa Elhoushi and Akshat Shrivastava and Diana Liskovich and Basil Hosmer and Bram Wasti and Liangzhen Lai and Anas Mahmoud and Bilge Acun and Saurabh Agarwal and Ahmed Roman and Ahmed A Aly and Beidi Chen and Carole-Jean Wu},
1645
+ title={LayerSkip: Enabling Early Exit Inference and Self-Speculative Decoding},
1646
+ year={2024},
1647
+ pages={12622-12642},
1648
+ url={https://doi.org/10.18653/v1/2024.acl-long.681},
1649
+ booktitle={Proceedings of the 62nd Annual Meeting of the Association for Computational Linguistics}
1650
+ }
1651
+
1652
+ @inproceedings{ye-etal-2021-tr,
1653
+ title = "TR-BERT: Dynamic Token Reduction for Accelerating BERT Inference",
1654
+ author = "Ye, Deming and
1655
+ Lin, Yankai and
1656
+ Huang, Yufei and
1657
+ Sun, Maosong",
1658
+ editor = "Toutanova, Kristina and
1659
+ Rumshisky, Anna and
1660
+ Zettlemoyer, Luke and
1661
+ Hakkani-Tur, Dilek and
1662
+ Beltagy, Iz and
1663
+ Bethard, Steven and
1664
+ Cotterell, Ryan and
1665
+ Chakraborty, Tanmoy and
1666
+ Zhou, Yichao",
1667
+ booktitle = "Proceedings of the 2021 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies",
1668
+ month = jun,
1669
+ year = "2021",
1670
+ address = "Online",
1671
+ publisher = "Association for Computational Linguistics",
1672
+ url = "https://aclanthology.org/2021.naacl-main.463/",
1673
+ doi = "10.18653/v1/2021.naacl-main.463",
1674
+ pages = "5798--5809"
1675
+ }
1676
+
1677
+ @misc{raposo2024mixtureofdepths,
1678
+ title={Mixture-of-Depths: Dynamically allocating compute in transformer-based language models},
1679
+ author={David Raposo and Sam Ritter and Blake Richards and Timothy Lillicrap and Peter Conway Humphreys and Adam Santoro},
1680
+ year={2024},
1681
+ eprint={2404.02258},
1682
+ archivePrefix={arXiv},
1683
+ primaryClass={cs.LG},
1684
+ url={https://arxiv.org/abs/2404.02258},
1685
+ }
1686
+
1687
+ % Additional citations for ICML version
1688
+ @inproceedings{vig2020causal,
1689
+ title={Investigating gender bias in language models using causal mediation analysis},
1690
+ author={Vig, Jesse and Gehrmann, Sebastian and Belinkov, Yonatan and Qian, Sharon and Nevo, Daniel and Singer, Yaron and Shieber, Stuart},
1691
+ booktitle={Advances in Neural Information Processing Systems},
1692
+ volume={33},
1693
+ pages={12388--12401},
1694
+ year={2020}
1695
+ }
1696
+
1697
+ @inproceedings{meng2022locating,
1698
+ title={Locating and editing factual associations in GPT},
1699
+ author={Meng, Kevin and Bau, David and Andonian, Alex and Belinkov, Yonatan},
1700
+ booktitle={Advances in Neural Information Processing Systems},
1701
+ volume={35},
1702
+ pages={17359--17372},
1703
+ year={2022}
1704
+ }
1705
+
1706
+ @article{team2024gemma,
1707
+ title={Gemma: Open models based on Gemini research and technology},
1708
+ author={Team, Gemma and Mesnard, Thomas and Hardin, Cassidy and Dadashi, Robert and Bhupatiraju, Surya and Pathak, Shreya and Sifre, Laurent and Rivi{\`e}re, Morgane and Kale, Mihir Sanjay and Love, Juliette and others},
1709
+ journal={arXiv preprint arXiv:2403.08295},
1710
+ year={2024}
1711
+ }
1712
+
1713
+ @article{lieberum2024gemma,
1714
+ title={Gemma scope: Open sparse autoencoders everywhere all at once on Gemma 2},
1715
+ author={Lieberum, Tom and Rajamanoharan, Senthooran and Conmy, Arthur and Smith, Lewis and Sonnerat, Nicolas and Varma, Vikrant and Kram{\'a}r, J{\'a}nos and Dragan, Anca and Shah, Rohin and Nanda, Neel},
1716
+ journal={arXiv preprint arXiv:2408.05147},
1717
+ year={2024}
1718
+ }
1719
+
1720
+ @article{engels2025decomposing,
1721
+ title={Decomposing the Dark Matter of Sparse Autoencoders},
1722
+ author={Engels, Joshua and Riggs, Logan and Tegmark, Max},
1723
+ journal={Transactions on Machine Learning Research},
1724
+ year={2025}
1725
+ }
1726
+
1727
+ @inproceedings{wu2025axbench,
1728
+ title={{AxBench}: Steering {LLM}s? Even Simple Baselines Outperform Sparse Autoencoders},
1729
+ author={Wu, Zhengxuan and Arora, Aryaman and Geiger, Atticus and Huang, Jing and Wang, Zheng and Potts, Christopher and Manning, Christopher D. and Bowman, Samuel R.},
1730
+ booktitle={ICML},
1731
+ year={2025}
1732
+ }
1733
+ @inproceedings{wang2023interpretability,
1734
+ title={Interpretability in the Wild: a Circuit for Indirect Object Identification in {GPT}-2 Small},
1735
+ author={Kevin Ro Wang and Alexandre Variengien and Arthur Conmy and Buck Shlegeris and Jacob Steinhardt},
1736
+ booktitle={The Eleventh International Conference on Learning Representations },
1737
+ year={2023},
1738
+ url={https://openreview.net/forum?id=NpsVSN6o4ul}
1739
+ }
app/src/content/embeds/banner.html ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="hero-3d-container"></div>
2
+ <style>
3
+ .hero-3d-container {
4
+ width: 100%; height: 440px; position: relative; overflow: hidden;
5
+ background: linear-gradient(135deg, #0c1220 0%, #0a0e1a 60%, #0d1424 100%);
6
+ }
7
+ .hero-3d-container > canvas { display: block; }
8
+ </style>
9
+ <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
11
+ <script>
12
+ (function() {
13
+ var ctr = document.querySelectorAll('.hero-3d-container');
14
+ var el = ctr[ctr.length - 1];
15
+ if (!el || el.dataset.m) return;
16
+ el.dataset.m = '1';
17
+
18
+ var W = el.clientWidth, H = el.clientHeight;
19
+
20
+ /* ===== Scene ===== */
21
+ var scene = new THREE.Scene();
22
+
23
+ /* ===== Camera ===== */
24
+ var camera = new THREE.PerspectiveCamera(44, W / H, 0.1, 100);
25
+ camera.position.set(7.5, 7, 19);
26
+
27
+ /* ===== Renderer ===== */
28
+ var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
29
+ renderer.setSize(W, H);
30
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
31
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
32
+ renderer.toneMappingExposure = 1.2;
33
+ el.appendChild(renderer.domElement);
34
+
35
+ /* ===== Lights ===== */
36
+ scene.add(new THREE.AmbientLight(0x9aadcc, 0.7));
37
+ var sun = new THREE.DirectionalLight(0xffffff, 0.9);
38
+ sun.position.set(5, 12, 8);
39
+ scene.add(sun);
40
+ var fill = new THREE.DirectionalLight(0x6688cc, 0.35);
41
+ fill.position.set(-5, 4, -4);
42
+ scene.add(fill);
43
+ var rim = new THREE.DirectionalLight(0x8866cc, 0.2);
44
+ rim.position.set(0, -2, 8);
45
+ scene.add(rim);
46
+
47
+ /* ===== Controls ===== */
48
+ var ctrl = new THREE.OrbitControls(camera, renderer.domElement);
49
+ ctrl.target.set(7.5, 1.5, 0);
50
+ ctrl.enableDamping = true;
51
+ ctrl.dampingFactor = 0.08;
52
+ ctrl.autoRotate = true;
53
+ ctrl.autoRotateSpeed = 0.3;
54
+ ctrl.enablePan = false;
55
+ ctrl.minDistance = 10;
56
+ ctrl.maxDistance = 28;
57
+ ctrl.maxPolarAngle = Math.PI * 0.72;
58
+ ctrl.update();
59
+
60
+ /* ===== Materials (paper-matched colors) ===== */
61
+ function M(c, e, ei) {
62
+ return new THREE.MeshStandardMaterial({
63
+ color: c, roughness: 0.4, metalness: 0.1,
64
+ emissive: e || 0x000000, emissiveIntensity: ei || 0
65
+ });
66
+ }
67
+ var GR = M(0x808080, 0x000000, 0); // gray sides (paper)
68
+ var DK = M(0x1a1a1a, 0x000000, 0); // black faces (paper)
69
+ var GN = M(0x4ade80, 0x22c55e, 0.6); // bright green separators
70
+ var RD = M(0xef4444, 0xff0000, 0.5); // bright red
71
+ var CY = M(0x22d3ee, 0x06b6d4, 0.5); // bright cyan
72
+ var BL = M(0x3b82f6, 0x2563eb, 0.5); // bright blue
73
+ var IN = M(0x818cf8, 0x6366f1, 0.5); // indigo
74
+ var YL = M(0xfde047, 0xeab308, 0.6); // bright yellow
75
+ var OR = M(0xf97316, 0xea580c, 0.5); // orange
76
+ var PK = M(0xf472b6, 0xec4899, 0.5); // pink
77
+ var PU = M(0xa855f7, 0x7c3aed, 0.5); // purple
78
+ var GK = M(0x2dd4bf, 0x14b8a6, 0.5); // teal (reward)
79
+
80
+ /* ===== Geometry ===== */
81
+ var BX = new THREE.BoxGeometry(0.85, 0.85, 0.85);
82
+ var ST = new THREE.BoxGeometry(0.12, 0.85, 0.06);
83
+ var EG = new THREE.EdgesGeometry(BX);
84
+ var EM = new THREE.LineBasicMaterial({ color: 0x1e293b, transparent: true, opacity: 0.3 });
85
+
86
+ /* ===== Text Sprites ===== */
87
+ function textSprite(text, opts) {
88
+ opts = opts || {};
89
+ var fontSize = opts.size || 28;
90
+ var color = opts.color || '#d4d4d4';
91
+ var bold = opts.bold || false;
92
+ var dpr = 2;
93
+ var cvs = document.createElement('canvas');
94
+ var ctx = cvs.getContext('2d');
95
+ var font = (bold ? 'bold ' : '') + (fontSize * dpr) + 'px Inter,-apple-system,system-ui,sans-serif';
96
+ ctx.font = font;
97
+ var w = ctx.measureText(text).width + 12 * dpr;
98
+ var h = fontSize * 1.8 * dpr;
99
+ cvs.width = Math.ceil(w);
100
+ cvs.height = Math.ceil(h);
101
+ ctx.font = font;
102
+ ctx.fillStyle = color;
103
+ ctx.textBaseline = 'middle';
104
+ ctx.textAlign = 'center';
105
+ ctx.fillText(text, w / 2, h / 2);
106
+ var tex = new THREE.CanvasTexture(cvs);
107
+ tex.minFilter = THREE.LinearFilter;
108
+ var mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: false, depthWrite: false });
109
+ var sprite = new THREE.Sprite(mat);
110
+ var sc = opts.scale || 0.6;
111
+ sprite.scale.set(sc * w / h, sc, 1);
112
+ sprite.renderOrder = 999;
113
+ return sprite;
114
+ }
115
+
116
+ function addLabel(text, x, y, z, opts) {
117
+ var s = textSprite(text, opts);
118
+ s.position.set(x, y, z || 0);
119
+ scene.add(s);
120
+ return s;
121
+ }
122
+
123
+ /* ===== Build Layer Stack ===== */
124
+ var greenSeps = [];
125
+ function buildStack(bx, hl) {
126
+ for (var y = 0; y < 4; y++) {
127
+ for (var x = 0; x < 4; x++) {
128
+ var k = x + ',' + y;
129
+ var top = (hl && hl[k]) || DK;
130
+ // Paper: black top/bottom, gray sides, colored top highlights
131
+ var m = new THREE.Mesh(BX, [GR, GR, top, DK, DK, DK]);
132
+ m.position.set(bx + x, y, 0);
133
+ scene.add(m);
134
+ var e = new THREE.LineSegments(EG, EM);
135
+ e.position.set(bx + x, y, 0);
136
+ scene.add(e);
137
+ }
138
+ var s = new THREE.Mesh(ST, GN.clone());
139
+ s.position.set(bx - 0.47, y, 0.43);
140
+ scene.add(s);
141
+ greenSeps.push(s);
142
+ }
143
+ }
144
+
145
+ // Policy stack with colored highlights
146
+ buildStack(0, {
147
+ '0,3': RD, '1,3': RD,
148
+ '2,2': CY, '3,2': CY,
149
+ '2,3': CY, '3,3': CY
150
+ });
151
+
152
+ // Critic stack with yellow top highlights (paper)
153
+ buildStack(9, {
154
+ '0,3': YL, '1,3': YL, '2,3': YL, '3,3': YL,
155
+ '0,2': YL, '1,2': YL
156
+ });
157
+
158
+ /* ===== Feature Direction Cubes (interactive) ===== */
159
+ var fMeshes = [], fGroups = [];
160
+ var fCols = [[RD, CY], [BL, RD], [CY, IN]];
161
+ var fDirs = [
162
+ [1, 0, 0.5], [-0.7, 0, 1],
163
+ [1, 0, -0.3], [0, 0, 1],
164
+ [-1, 0, 0.2], [0.7, 0, 0.7]
165
+ ];
166
+
167
+ for (var fy = 0; fy < 3; fy++) {
168
+ for (var fx = 0; fx < 2; fx++) {
169
+ var g = new THREE.Group();
170
+ var px = 5 + fx, py = fy + 0.5;
171
+ g.position.set(px, py, 0);
172
+
173
+ var col = fCols[fy][fx];
174
+ var fm = new THREE.Mesh(BX, [GR, GR, col, DK, DK, DK]);
175
+ g.add(fm);
176
+ g.add(new THREE.LineSegments(EG, EM));
177
+
178
+ var di = fy * 2 + fx;
179
+ var d = new THREE.Vector3(fDirs[di][0], fDirs[di][1], fDirs[di][2]).normalize();
180
+ g.add(new THREE.ArrowHelper(d, new THREE.Vector3(0, 0.48, 0), 0.45, 0x60a5fa, 0.14, 0.1));
181
+
182
+ g.userData = { baseY: py, targetY: py };
183
+ scene.add(g);
184
+ fMeshes.push(fm);
185
+ fGroups.push(g);
186
+ }
187
+ }
188
+
189
+ /* ===== Critic Value / Advantage / Reward ===== */
190
+ var criticCubes = [];
191
+ function addSingle(pos, mats) {
192
+ var m = new THREE.Mesh(BX, mats);
193
+ m.position.copy(pos);
194
+ scene.add(m);
195
+ var e = new THREE.LineSegments(EG, EM);
196
+ e.position.copy(pos);
197
+ scene.add(e);
198
+ criticCubes.push({ mesh: m, baseY: pos.y });
199
+ return m;
200
+ }
201
+
202
+ addSingle(new THREE.Vector3(14, 3, 0), [OR, OR, YL, OR, PK, PK]);
203
+ addSingle(new THREE.Vector3(15.5, 1.5, 0), [OR, DK, PU, DK, DK, DK]);
204
+ addSingle(new THREE.Vector3(14, 0, 0), [GK, GK, GN, GK, GK, GK]);
205
+
206
+ /* ===== Particles (residual stream flow) ===== */
207
+ var particles = [];
208
+ var particleGeo = new THREE.SphereGeometry(0.06, 8, 8);
209
+ var particlePaths = [
210
+ // Policy path: tokens β†’ policy stack β†’ features β†’ back
211
+ { start: [-1.5, -0.5, 0], end: [1.5, 3.5, 0], color: 0x4ade80, speed: 0.008 },
212
+ { start: [1.5, 3.5, 0], end: [5.5, 1.5, 0], color: 0x60a5fa, speed: 0.007 },
213
+ { start: [5.5, 1.5, 0], end: [1.5, -0.5, 0], color: 0x818cf8, speed: 0.009 },
214
+ // Critic path: tokens β†’ critic stack β†’ value β†’ advantage
215
+ { start: [7.5, -0.5, 0], end: [10.5, 3.5, 0], color: 0xfde047, speed: 0.008 },
216
+ { start: [10.5, 3.5, 0], end: [14, 3, 0], color: 0xfbbf24, speed: 0.007 },
217
+ { start: [14, 3, 0], end: [15.5, 1.5, 0], color: 0xa78bfa, speed: 0.006 },
218
+ { start: [15.5, 1.5, 0], end: [14, 0, 0], color: 0x4ade80, speed: 0.008 }
219
+ ];
220
+
221
+ for (var pi = 0; pi < particlePaths.length; pi++) {
222
+ var pp = particlePaths[pi];
223
+ var pMat = new THREE.MeshBasicMaterial({ color: pp.color, transparent: true, opacity: 0.9 });
224
+ var pMesh = new THREE.Mesh(particleGeo, pMat);
225
+ pMesh.position.set(pp.start[0], pp.start[1], pp.start[2]);
226
+ scene.add(pMesh);
227
+
228
+ // Add glow sprite
229
+ var glowCvs = document.createElement('canvas');
230
+ glowCvs.width = 32; glowCvs.height = 32;
231
+ var glowCtx = glowCvs.getContext('2d');
232
+ var grad = glowCtx.createRadialGradient(16, 16, 0, 16, 16, 16);
233
+ var c = new THREE.Color(pp.color);
234
+ grad.addColorStop(0, 'rgba(' + Math.round(c.r*255) + ',' + Math.round(c.g*255) + ',' + Math.round(c.b*255) + ',0.6)');
235
+ grad.addColorStop(1, 'rgba(' + Math.round(c.r*255) + ',' + Math.round(c.g*255) + ',' + Math.round(c.b*255) + ',0)');
236
+ glowCtx.fillStyle = grad;
237
+ glowCtx.fillRect(0, 0, 32, 32);
238
+ var glowTex = new THREE.CanvasTexture(glowCvs);
239
+ var glowMat = new THREE.SpriteMaterial({ map: glowTex, transparent: true, depthTest: false });
240
+ var glow = new THREE.Sprite(glowMat);
241
+ glow.scale.set(0.5, 0.5, 1);
242
+ pMesh.add(glow);
243
+
244
+ particles.push({
245
+ mesh: pMesh, mat: pMat,
246
+ start: new THREE.Vector3(pp.start[0], pp.start[1], pp.start[2]),
247
+ end: new THREE.Vector3(pp.end[0], pp.end[1], pp.end[2]),
248
+ t: Math.random(), speed: pp.speed
249
+ });
250
+ }
251
+
252
+ /* ===== Labels ===== */
253
+ addLabel('Policy \u03C0\u03B8', 1.5, 5, 0, { size: 36, color: '#a5b4fc', bold: true, scale: 0.7 });
254
+ addLabel('Critic V\u03C6', 10.5, 5, 0, { size: 36, color: '#fcd34d', bold: true, scale: 0.7 });
255
+
256
+ addLabel('1. Observe residual stream', 1.5, -1.2, 0, { size: 20, color: '#525252', scale: 0.5 });
257
+ addLabel('2. Select feature', 5.5, 3.8, 0, { size: 20, color: '#525252', scale: 0.5 });
258
+ addLabel('3. Append direction', 3, -2.1, 0, { size: 20, color: '#525252', scale: 0.5 });
259
+
260
+ addLabel('1. Observe residual stream', 10.5, -1.2, 0, { size: 20, color: '#525252', scale: 0.5 });
261
+ addLabel('2. Estimate state value', 14.5, 4.3, 0, { size: 20, color: '#525252', scale: 0.5 });
262
+ addLabel('3. Calculate advantage', 15, -0.8, 0, { size: 20, color: '#525252', scale: 0.5 });
263
+
264
+ addLabel('SAE Feature Directions', 5.5, -0.8, 0, { size: 22, color: '#737373', scale: 0.5 });
265
+ addLabel('Critic Value', 14, 2, 0, { size: 22, color: '#fbbf24', scale: 0.5 });
266
+ addLabel('Advantage', 15.5, 0.4, 0, { size: 22, color: '#a78bfa', scale: 0.5 });
267
+ addLabel('Verifiable Reward', 14, -1.3, 0, { size: 22, color: '#34d399', scale: 0.5 });
268
+ addLabel('1/0', 14.6, 0, 0.55, { size: 24, color: '#4ade80', bold: true, scale: 0.4 });
269
+
270
+ var tks = ['John', 'and', 'Merry', 'went'];
271
+ var tc = ['#4ade80', '#fde68a', '#4ade80', '#fde68a'];
272
+ for (var i = 0; i < 4; i++) {
273
+ addLabel(tks[i], -1.5, i, 0, { size: 24, color: tc[i], bold: true, scale: 0.45 });
274
+ addLabel(tks[i], 7.5, i, 0, { size: 24, color: tc[i], bold: true, scale: 0.45 });
275
+ }
276
+
277
+ /* ===== Click: Select Feature ===== */
278
+ var ray = new THREE.Raycaster();
279
+ var mp = new THREE.Vector2();
280
+ var sel = null;
281
+ var selSprite = textSprite('\u2713 selected', { size: 22, color: '#818cf8', bold: true, scale: 0.4 });
282
+ selSprite.visible = false;
283
+ scene.add(selSprite);
284
+
285
+ el.addEventListener('click', function(ev) {
286
+ var r = el.getBoundingClientRect();
287
+ mp.x = ((ev.clientX - r.left) / r.width) * 2 - 1;
288
+ mp.y = -((ev.clientY - r.top) / r.height) * 2 + 1;
289
+ ray.setFromCamera(mp, camera);
290
+ var h = ray.intersectObjects(fMeshes);
291
+ if (h.length) {
292
+ var grp = h[0].object.parent;
293
+ if (sel === grp) {
294
+ grp.userData.targetY = grp.userData.baseY;
295
+ sel = null;
296
+ selSprite.visible = false;
297
+ } else {
298
+ if (sel) sel.userData.targetY = sel.userData.baseY;
299
+ sel = grp;
300
+ grp.userData.targetY = grp.userData.baseY + 0.35;
301
+ selSprite.visible = true;
302
+ }
303
+ }
304
+ });
305
+
306
+ el.addEventListener('mousemove', function(ev) {
307
+ var r = el.getBoundingClientRect();
308
+ mp.x = ((ev.clientX - r.left) / r.width) * 2 - 1;
309
+ mp.y = -((ev.clientY - r.top) / r.height) * 2 + 1;
310
+ ray.setFromCamera(mp, camera);
311
+ renderer.domElement.style.cursor = ray.intersectObjects(fMeshes).length ? 'pointer' : '';
312
+ });
313
+
314
+ /* ===== Render Loop ===== */
315
+ var on = true;
316
+ var clock = new THREE.Clock();
317
+
318
+ function loop() {
319
+ requestAnimationFrame(loop);
320
+ if (!on) return;
321
+ var t = clock.getElapsedTime();
322
+ ctrl.update();
323
+
324
+ /* --- Green separator pulse --- */
325
+ for (var si = 0; si < greenSeps.length; si++) {
326
+ var sep = greenSeps[si];
327
+ var pulse = 0.4 + 0.4 * Math.sin(t * 2.5 + si * 0.8);
328
+ sep.material.emissiveIntensity = pulse;
329
+ }
330
+
331
+ /* --- Feature cube gentle float --- */
332
+ for (var i = 0; i < fGroups.length; i++) {
333
+ var g = fGroups[i];
334
+ var floatOffset = Math.sin(t * 1.2 + i * 1.1) * 0.06;
335
+ var targetWithFloat = g.userData.targetY + floatOffset;
336
+ var dy = targetWithFloat - g.position.y;
337
+ if (Math.abs(dy) > 0.001) g.position.y += dy * 0.12;
338
+ }
339
+
340
+ /* --- Critic cubes gentle bob --- */
341
+ for (var ci = 0; ci < criticCubes.length; ci++) {
342
+ var cc = criticCubes[ci];
343
+ cc.mesh.position.y = cc.baseY + Math.sin(t * 1.5 + ci * 2.0) * 0.04;
344
+ }
345
+
346
+ /* --- Particle flow --- */
347
+ for (var pi = 0; pi < particles.length; pi++) {
348
+ var p = particles[pi];
349
+ p.t += p.speed;
350
+ if (p.t > 1) p.t -= 1;
351
+ // Smooth ease in-out
352
+ var ease = p.t < 0.5 ? 2 * p.t * p.t : 1 - Math.pow(-2 * p.t + 2, 2) / 2;
353
+ p.mesh.position.lerpVectors(p.start, p.end, ease);
354
+ // Slight arc upward
355
+ p.mesh.position.y += Math.sin(p.t * Math.PI) * 0.4;
356
+ // Fade at endpoints
357
+ var fade = Math.sin(p.t * Math.PI);
358
+ p.mat.opacity = 0.3 + 0.7 * fade;
359
+ // Scale pulse
360
+ var sc = 0.8 + 0.4 * fade;
361
+ p.mesh.scale.set(sc, sc, sc);
362
+ }
363
+
364
+ if (sel && selSprite.visible) {
365
+ selSprite.position.set(sel.position.x, sel.position.y + 0.85, sel.position.z);
366
+ }
367
+
368
+ renderer.render(scene, camera);
369
+ }
370
+ loop();
371
+
372
+ /* ===== Resize ===== */
373
+ if ('ResizeObserver' in window) {
374
+ new ResizeObserver(function() {
375
+ W = el.clientWidth; H = el.clientHeight;
376
+ camera.aspect = W / H;
377
+ camera.updateProjectionMatrix();
378
+ renderer.setSize(W, H);
379
+ }).observe(el);
380
+ }
381
+
382
+ /* ===== Visibility ===== */
383
+ if ('IntersectionObserver' in window) {
384
+ new IntersectionObserver(function(entries) {
385
+ on = entries[0].isIntersecting;
386
+ }, { threshold: 0.05 }).observe(el);
387
+ }
388
+ })();
389
+ </script>
app/src/content/embeds/baseline-comparison.html ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="baseline-comparison"></div>
2
+ <style>
3
+ .baseline-comparison {
4
+ font-family: system-ui, -apple-system, sans-serif;
5
+ padding: 0;
6
+ }
7
+
8
+ .baseline-comparison__wrapper {
9
+ display: grid;
10
+ grid-template-columns: 1fr 1fr;
11
+ gap: 20px;
12
+ }
13
+
14
+ @media (max-width: 640px) {
15
+ .baseline-comparison__wrapper {
16
+ grid-template-columns: 1fr;
17
+ }
18
+ }
19
+
20
+ .baseline-comparison__chart-card {
21
+ border: 1px solid var(--border-color);
22
+ border-radius: 16px;
23
+ padding: 24px;
24
+ background: var(--page-bg);
25
+ }
26
+
27
+ .baseline-comparison__chart-title {
28
+ font-size: 15px;
29
+ font-weight: 700;
30
+ color: var(--text-color);
31
+ margin-bottom: 16px;
32
+ text-align: center;
33
+ }
34
+
35
+ .baseline-comparison svg {
36
+ width: 100%;
37
+ height: auto;
38
+ display: block;
39
+ overflow: visible;
40
+ }
41
+
42
+ .baseline-comparison .axis path { display: none; }
43
+ .baseline-comparison .axis line { stroke: var(--grid-color); }
44
+ .baseline-comparison .axis text { fill: var(--tick-color); font-size: 12px; }
45
+ .baseline-comparison .grid line { stroke: var(--grid-color); stroke-width: 1; }
46
+
47
+ .baseline-comparison__bar {
48
+ transition: opacity 0.2s ease;
49
+ rx: 4;
50
+ ry: 4;
51
+ }
52
+
53
+ .baseline-comparison__bar--ours {
54
+ filter: drop-shadow(0 2px 4px rgba(79, 70, 229, 0.3));
55
+ }
56
+
57
+ .baseline-comparison__tooltip {
58
+ position: absolute;
59
+ padding: 8px 14px;
60
+ background: var(--text-color);
61
+ color: var(--page-bg);
62
+ border-radius: 8px;
63
+ font-size: 13px;
64
+ pointer-events: none;
65
+ opacity: 0;
66
+ transition: opacity 0.15s ease;
67
+ white-space: nowrap;
68
+ z-index: 100;
69
+ }
70
+
71
+ .baseline-comparison__legend {
72
+ display: flex;
73
+ justify-content: center;
74
+ gap: 24px;
75
+ margin-bottom: 16px;
76
+ flex-wrap: wrap;
77
+ }
78
+
79
+ .baseline-comparison__legend-item {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 8px;
83
+ font-size: 12px;
84
+ color: var(--text-color);
85
+ }
86
+
87
+ .baseline-comparison__legend-swatch {
88
+ width: 14px;
89
+ height: 14px;
90
+ border-radius: 4px;
91
+ }
92
+ </style>
93
+ <script>
94
+ (function() {
95
+ function ensureD3(cb) {
96
+ if (window.d3 && typeof window.d3.select === 'function') return cb();
97
+ var s = document.getElementById('d3-cdn-script');
98
+ if (!s) {
99
+ s = document.createElement('script');
100
+ s.id = 'd3-cdn-script';
101
+ s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
102
+ document.head.appendChild(s);
103
+ }
104
+ s.addEventListener('load', function() { if (window.d3) cb(); }, { once: true });
105
+ }
106
+
107
+ function bootstrap() {
108
+ var containers = document.querySelectorAll('.baseline-comparison');
109
+ var root = containers[containers.length - 1];
110
+ if (!root || root.dataset.mounted) return;
111
+ root.dataset.mounted = '1';
112
+
113
+ const data = [
114
+ {method: "No Steering", bbq: 60.17, harm: 41.46, ours: false},
115
+ {method: "Random Feature", bbq: 58.36, harm: 45.35, ours: false},
116
+ {method: "Random + AFM", bbq: 60.16, harm: 46.96, ours: false},
117
+ {method: "Most-Active", bbq: 59.94, harm: 48.03, ours: false},
118
+ {method: "CRL (Ours)", bbq: 65.86, harm: 49.12, ours: true}
119
+ ];
120
+
121
+ const colors = {
122
+ base: '#a3a3a3',
123
+ ours: '#4F46E5'
124
+ };
125
+
126
+ function buildChart(container, title, key, domain) {
127
+ const card = document.createElement('div');
128
+ card.className = 'baseline-comparison__chart-card';
129
+ card.innerHTML = `<div class="baseline-comparison__chart-title">${title}</div>`;
130
+
131
+ const margin = {top: 8, right: 50, bottom: 30, left: 100};
132
+ const width = 340;
133
+ const height = data.length * 40 + margin.top + margin.bottom;
134
+
135
+ const svg = d3.select(card).append('svg')
136
+ .attr('viewBox', `0 0 ${width + margin.left + margin.right} ${height}`)
137
+ .attr('preserveAspectRatio', 'xMidYMid meet');
138
+
139
+ const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);
140
+
141
+ const x = d3.scaleLinear().domain(domain).range([0, width]);
142
+ const y = d3.scaleBand().domain(data.map(d => d.method)).range([0, height - margin.top - margin.bottom]).padding(0.35);
143
+
144
+ g.append('g').attr('class', 'grid')
145
+ .call(d3.axisBottom(x).ticks(5).tickSize(height - margin.top - margin.bottom).tickFormat(''))
146
+ .attr('stroke-opacity', 0.5)
147
+ .select('.domain').remove();
148
+
149
+ g.append('g').attr('class', 'axis')
150
+ .attr('transform', `translate(0,${height - margin.top - margin.bottom})`)
151
+ .call(d3.axisBottom(x).ticks(5).tickFormat(d => d + '%'));
152
+
153
+ g.append('g').attr('class', 'axis')
154
+ .call(d3.axisLeft(y).tickSize(0).tickPadding(10));
155
+
156
+ const bars = g.selectAll('.baseline-comparison__bar')
157
+ .data(data).enter().append('rect')
158
+ .attr('class', d => 'baseline-comparison__bar' + (d.ours ? ' baseline-comparison__bar--ours' : ''))
159
+ .attr('x', 0)
160
+ .attr('y', d => y(d.method))
161
+ .attr('height', y.bandwidth())
162
+ .attr('fill', d => d.ours ? colors.ours : colors.base)
163
+ .attr('rx', 4).attr('ry', 4)
164
+ .attr('width', 0);
165
+
166
+ const labels = g.selectAll('.bar-label')
167
+ .data(data).enter().append('text')
168
+ .attr('x', d => x(d[key]) + 6)
169
+ .attr('y', d => y(d.method) + y.bandwidth() / 2)
170
+ .attr('dy', '0.35em')
171
+ .attr('fill', 'var(--text-color)')
172
+ .attr('font-size', '12px')
173
+ .attr('font-weight', d => d.ours ? '700' : '400')
174
+ .attr('opacity', 0)
175
+ .text(d => d[key].toFixed(2) + '%');
176
+
177
+ const observer = new IntersectionObserver((entries) => {
178
+ entries.forEach(entry => {
179
+ if (entry.isIntersecting) {
180
+ bars.transition().duration(800).delay((d, i) => i * 100)
181
+ .attr('width', d => x(d[key]));
182
+ labels.transition().duration(400).delay((d, i) => i * 100 + 600)
183
+ .attr('opacity', 1);
184
+ observer.disconnect();
185
+ }
186
+ });
187
+ }, {threshold: 0.3});
188
+ observer.observe(card);
189
+
190
+ container.appendChild(card);
191
+ }
192
+
193
+ root.innerHTML = '<div class="baseline-comparison__wrapper"></div>';
194
+ const wrapper = root.querySelector('.baseline-comparison__wrapper');
195
+ buildChart(wrapper, 'BBQ Ambiguous (%)', 'bbq', [0, 72]);
196
+ buildChart(wrapper, 'HarmBench (%)', 'harm', [0, 55]);
197
+ }
198
+
199
+ ensureD3(bootstrap);
200
+ })();
201
+ </script>
app/src/content/embeds/branch-explorer.html ADDED
@@ -0,0 +1,805 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="branch-explorer"></div>
2
+
3
+ <style>
4
+ .branch-explorer {
5
+ font-family: var(--default-font-family, system-ui, -apple-system, sans-serif);
6
+ background: transparent;
7
+ border: none;
8
+ border-radius: 0;
9
+ padding: 0;
10
+ width: 100%;
11
+ margin: 0 auto;
12
+ --green: #10B981;
13
+ --red: #EF4444;
14
+ --blue: #3B82F6;
15
+ --purple: #8B5CF6;
16
+ --indigo: #4F46E5;
17
+ --green-bg: oklch(from #10B981 l c h / 0.08);
18
+ --red-bg: oklch(from #EF4444 l c h / 0.08);
19
+ --green-border: oklch(from #10B981 l c h / 0.25);
20
+ --red-border: oklch(from #EF4444 l c h / 0.25);
21
+ --mono: 'SF Mono', 'Fira Code', 'Cascadia Code', 'JetBrains Mono', monospace;
22
+ }
23
+
24
+ /* --- Navigation --- */
25
+ .branch-explorer .be-nav {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ gap: 12px;
30
+ margin-bottom: 20px;
31
+ }
32
+
33
+ .branch-explorer .be-nav-btn {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ width: 36px;
38
+ height: 36px;
39
+ border-radius: 999px;
40
+ border: 1px solid var(--border-color, #e5e7eb);
41
+ background: var(--surface-bg, #fff);
42
+ color: var(--text-color, #1f2937);
43
+ cursor: pointer;
44
+ font-size: 16px;
45
+ transition: all 0.2s;
46
+ padding: 0;
47
+ line-height: 1;
48
+ }
49
+
50
+ .branch-explorer .be-nav-btn:hover:not(:disabled) {
51
+ background: var(--border-color, #e5e7eb);
52
+ }
53
+
54
+ .branch-explorer .be-nav-btn:disabled {
55
+ opacity: 0.35;
56
+ cursor: not-allowed;
57
+ }
58
+
59
+ .branch-explorer .be-play-btn {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ gap: 5px;
64
+ height: 36px;
65
+ padding: 0 16px;
66
+ border-radius: 999px;
67
+ border: 1px solid var(--indigo, #4F46E5);
68
+ background: var(--indigo, #4F46E5);
69
+ color: #fff;
70
+ cursor: pointer;
71
+ font-size: 12px;
72
+ font-weight: 600;
73
+ font-family: inherit;
74
+ transition: all 0.2s;
75
+ line-height: 1;
76
+ }
77
+
78
+ .branch-explorer .be-play-btn:hover {
79
+ background: oklch(from #4F46E5 calc(l - 0.05) c h);
80
+ }
81
+
82
+ .branch-explorer .be-nav-label {
83
+ font-size: 14px;
84
+ font-weight: 500;
85
+ color: var(--muted-color, #6b7280);
86
+ min-width: 120px;
87
+ text-align: center;
88
+ user-select: none;
89
+ }
90
+
91
+ /* --- Question card --- */
92
+ .branch-explorer .be-question-card {
93
+ background: oklch(from #3B82F6 l c h / 0.06);
94
+ border: 1px solid oklch(from #3B82F6 l c h / 0.2);
95
+ border-radius: 16px;
96
+ padding: 14px 20px;
97
+ margin-bottom: 12px;
98
+ text-align: left;
99
+ }
100
+
101
+ .branch-explorer .be-question-label {
102
+ font-size: 11px;
103
+ font-weight: 600;
104
+ text-transform: uppercase;
105
+ letter-spacing: 0.05em;
106
+ color: var(--blue, #3B82F6);
107
+ margin-bottom: 6px;
108
+ }
109
+
110
+ .branch-explorer .be-question-text {
111
+ font-size: 14px;
112
+ color: var(--text-color, #1f2937);
113
+ line-height: 1.55;
114
+ }
115
+
116
+ /* --- Context card --- */
117
+ .branch-explorer .be-context-card {
118
+ background: var(--surface-bg, #f9fafb);
119
+ border: 1px solid var(--border-color, #e5e7eb);
120
+ border-radius: 16px;
121
+ padding: 16px 20px;
122
+ margin-bottom: 20px;
123
+ text-align: center;
124
+ }
125
+
126
+ .branch-explorer .be-context-label {
127
+ font-size: 11px;
128
+ font-weight: 600;
129
+ text-transform: uppercase;
130
+ letter-spacing: 0.05em;
131
+ color: var(--muted-color, #6b7280);
132
+ margin-bottom: 6px;
133
+ }
134
+
135
+ .branch-explorer .be-context-text {
136
+ font-size: 14px;
137
+ font-family: var(--mono);
138
+ color: var(--text-color, #1f2937);
139
+ line-height: 1.5;
140
+ word-break: break-word;
141
+ display: flex;
142
+ flex-wrap: wrap;
143
+ gap: 3px;
144
+ justify-content: center;
145
+ min-height: 22px;
146
+ }
147
+
148
+ /* --- SVG tree --- */
149
+ .branch-explorer .be-tree-wrap {
150
+ width: 100%;
151
+ margin-bottom: 20px;
152
+ display: flex;
153
+ justify-content: center;
154
+ opacity: 0;
155
+ transition: opacity 0.4s ease;
156
+ }
157
+
158
+ .branch-explorer .be-tree-wrap.visible {
159
+ opacity: 1;
160
+ }
161
+
162
+ .branch-explorer .be-tree-wrap svg {
163
+ display: block;
164
+ overflow: visible;
165
+ }
166
+
167
+ .branch-explorer .be-tree-wrap .branch-path {
168
+ fill: none;
169
+ stroke-width: 2.5;
170
+ stroke-linecap: round;
171
+ }
172
+
173
+ .branch-explorer .be-tree-wrap .node-label {
174
+ font-size: 11px;
175
+ font-weight: 500;
176
+ font-family: var(--default-font-family, system-ui, sans-serif);
177
+ }
178
+
179
+ /* --- Outputs grid --- */
180
+ .branch-explorer .be-outputs {
181
+ display: grid;
182
+ grid-template-columns: 1fr 1fr;
183
+ gap: 16px;
184
+ opacity: 0;
185
+ transform: translateY(8px);
186
+ transition: opacity 0.4s ease, transform 0.4s ease;
187
+ }
188
+
189
+ .branch-explorer .be-outputs.visible {
190
+ opacity: 1;
191
+ transform: translateY(0);
192
+ }
193
+
194
+ @media (max-width: 640px) {
195
+ .branch-explorer .be-outputs {
196
+ grid-template-columns: 1fr;
197
+ }
198
+ }
199
+
200
+ .branch-explorer .be-output-card {
201
+ border-radius: 16px;
202
+ padding: 16px;
203
+ border: 1.5px solid;
204
+ }
205
+
206
+ .branch-explorer .be-output-card.correct {
207
+ border-color: var(--green-border);
208
+ background: var(--green-bg);
209
+ }
210
+
211
+ .branch-explorer .be-output-card.incorrect {
212
+ border-color: var(--red-border);
213
+ background: var(--red-bg);
214
+ }
215
+
216
+ .branch-explorer .be-output-header {
217
+ display: flex;
218
+ align-items: center;
219
+ gap: 8px;
220
+ margin-bottom: 10px;
221
+ flex-wrap: wrap;
222
+ }
223
+
224
+ .branch-explorer .be-output-badge {
225
+ display: inline-flex;
226
+ align-items: center;
227
+ gap: 0;
228
+ font-size: 12px;
229
+ font-weight: 600;
230
+ padding: 0;
231
+ border-radius: 999px;
232
+ overflow: hidden;
233
+ line-height: 1;
234
+ flex-shrink: 0;
235
+ }
236
+
237
+ .branch-explorer .be-output-badge .badge-icon {
238
+ padding: 4px 7px;
239
+ color: #fff;
240
+ font-size: 13px;
241
+ }
242
+
243
+ .branch-explorer .be-output-badge.correct .badge-icon { background: var(--green); }
244
+ .branch-explorer .be-output-badge.incorrect .badge-icon { background: var(--red); }
245
+
246
+ .branch-explorer .be-output-badge .badge-label {
247
+ padding: 4px 10px 4px 8px;
248
+ font-size: 11px;
249
+ font-weight: 600;
250
+ text-transform: uppercase;
251
+ letter-spacing: 0.04em;
252
+ }
253
+
254
+ .branch-explorer .be-output-badge.correct .badge-label {
255
+ background: oklch(from #10B981 l c h / 0.15);
256
+ color: var(--green);
257
+ }
258
+
259
+ .branch-explorer .be-output-badge.incorrect .badge-label {
260
+ background: oklch(from #EF4444 l c h / 0.15);
261
+ color: var(--red);
262
+ }
263
+
264
+ .branch-explorer .be-feature-pill {
265
+ display: inline-flex;
266
+ align-items: center;
267
+ gap: 0;
268
+ font-size: 12px;
269
+ font-weight: 600;
270
+ border-radius: 999px;
271
+ overflow: hidden;
272
+ line-height: 1;
273
+ cursor: default;
274
+ position: relative;
275
+ flex-shrink: 0;
276
+ }
277
+
278
+ .branch-explorer .be-feature-pill .fp-layer {
279
+ padding: 4px 7px;
280
+ color: #fff;
281
+ font-size: 11px;
282
+ font-weight: 700;
283
+ font-family: var(--mono);
284
+ }
285
+
286
+ .branch-explorer .be-feature-pill .fp-layer.layer-blue { background: var(--blue); }
287
+ .branch-explorer .be-feature-pill .fp-layer.layer-purple { background: var(--purple); }
288
+
289
+ .branch-explorer .be-feature-pill .fp-id {
290
+ padding: 4px 8px 4px 6px;
291
+ font-size: 11px;
292
+ font-weight: 600;
293
+ font-family: var(--mono);
294
+ background: var(--surface-bg, #f3f4f6);
295
+ color: var(--text-color, #374151);
296
+ border: 1px solid var(--border-color, #e5e7eb);
297
+ border-left: none;
298
+ border-radius: 0 999px 999px 0;
299
+ }
300
+
301
+ .branch-explorer .be-feature-pill .fp-tooltip {
302
+ display: none;
303
+ position: absolute;
304
+ top: calc(100% + 6px);
305
+ left: 50%;
306
+ transform: translateX(-50%);
307
+ background: var(--text-color, #1f2937);
308
+ color: var(--page-bg, #fff);
309
+ font-size: 11px;
310
+ font-weight: 400;
311
+ font-family: var(--default-font-family, system-ui, sans-serif);
312
+ padding: 6px 10px;
313
+ border-radius: 8px;
314
+ white-space: normal;
315
+ max-width: 260px;
316
+ z-index: 10;
317
+ pointer-events: none;
318
+ line-height: 1.4;
319
+ }
320
+
321
+ .branch-explorer .be-feature-pill:hover .fp-tooltip { display: block; }
322
+
323
+ /* --- Token pills --- */
324
+ .branch-explorer .be-token {
325
+ display: inline-block;
326
+ font-family: var(--mono);
327
+ font-size: 12px;
328
+ line-height: 1.3;
329
+ padding: 3px 6px;
330
+ border-radius: 5px;
331
+ white-space: pre;
332
+ opacity: 0;
333
+ transform: scale(0.7);
334
+ transition: opacity 0.1s ease, transform 0.1s ease;
335
+ }
336
+
337
+ .branch-explorer .be-token.visible {
338
+ opacity: 1;
339
+ transform: scale(1);
340
+ }
341
+
342
+ .branch-explorer .be-token.neutral {
343
+ background: oklch(from #6b7280 l c h / 0.08);
344
+ color: var(--text-color, #374151);
345
+ }
346
+
347
+ .branch-explorer .be-token.tok-green {
348
+ background: oklch(from #10B981 l c h / 0.13);
349
+ color: var(--text-color, #374151);
350
+ }
351
+
352
+ .branch-explorer .be-token.tok-red {
353
+ background: oklch(from #EF4444 l c h / 0.13);
354
+ color: var(--text-color, #374151);
355
+ }
356
+
357
+
358
+ /* --- Output token rows --- */
359
+ .branch-explorer .be-output-tokens {
360
+ display: flex;
361
+ flex-wrap: wrap;
362
+ gap: 3px;
363
+ align-items: flex-start;
364
+ min-height: 24px;
365
+ }
366
+
367
+ /* --- Progress bar --- */
368
+ .branch-explorer .be-progress {
369
+ width: 100%;
370
+ height: 3px;
371
+ background: oklch(from #6b7280 l c h / 0.08);
372
+ border-radius: 2px;
373
+ margin-bottom: 16px;
374
+ overflow: hidden;
375
+ }
376
+
377
+ .branch-explorer .be-progress-fill {
378
+ height: 100%;
379
+ background: var(--indigo, #4F46E5);
380
+ border-radius: 2px;
381
+ width: 0%;
382
+ transition: width 0.15s linear;
383
+ }
384
+ </style>
385
+
386
+ <script>
387
+ (function() {
388
+ if (!window.d3) {
389
+ var s = document.createElement('script');
390
+ s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
391
+ s.onload = init;
392
+ document.head.appendChild(s);
393
+ } else { setTimeout(init, 0); }
394
+
395
+ function init() {
396
+ var BRANCHES = [{"context": "Here ' s how we can solve it : 1 . ** Calculate daily rice consumption :** ", "correct": {"feature": "L20/11163", "description": "phrases and terms related to assessments, evaluations, or strategic plans", "output": " - Each person consumes 0 . 2 kg per meal * 2 meals = 0 . 4 kg per day . - Total consumption per day : "}, "incorrect": {"feature": "L10/8428", "description": " phrases related to clinical examinations and diagnostic procedures in healthcare", "output": "5 members * 0 . 2 kg / member = 1 kg of rice per day . 2 . ** Calculate weekly rice consumption :** 1 kg / day * "}, "question": "5 family members each eat 0.2 kg of rice per meal, with 2 meals per day. How long will a 42 kg bag of rice last?"}, {"context": "Here ' s how we can break down the problem : 1 . ** Betty ' s points :**", "correct": {"feature": "L10/11074", "description": "references to significant individuals and their relationships", "output": " - Betty collected 3 0 % more than Adam , who has 5 0 points . - 3 0 % of 5 0 points = ( 3"}, "incorrect": {"feature": "L20/8693", "description": "expressions of pride and accomplishment", "output": "Betty collected 3 0 % more than Adam , so she collected 1 . 3 0 * 5 0 = 6 5 points more than Adam . * Betty"}, "question": "Adam has 50 points. Betty collected 30% more than Adam. Marta collected 3 times Tom's points. Tom has 30 fewer than Betty. How many points to reach the 400-point trip threshold?"}, {"context": "Here ' s how we can solve it : ", "correct": {"feature": "L10/16145", "description": " mathematical symbols and notation", "output": "1 . ** Morning :** 5 0 people entered the restaurant . 2 . ** 1 0 : 0 0 :** 4 0 more people entered , bringing the total to "}, "incorrect": {"feature": "L20/11861", "description": "phrases related to food and dining experiences", "output": "* ** Step 1 : People at 1 0 : 0 0 ** - 5 0 people entered in the morning . * ** Step 2"}, "question": "50 people enter a restaurant in the morning. 40 more come at 10:00. Some leave by lunch, and more arrive in the afternoon. How many at closing?"}];
397
+
398
+ var containers = document.querySelectorAll('.branch-explorer');
399
+ for (var ci = 0; ci < containers.length; ci++) { mountBranchExplorer(containers[ci]); }
400
+ function mountBranchExplorer(container) {
401
+ if (!container || container.dataset.mounted) return;
402
+ container.dataset.mounted = 'true';
403
+
404
+ /* ---------- STATE ---------- */
405
+ var currentIndex = 0;
406
+ var playing = false;
407
+ var phase = 'idle'; // idle, context, branch-pause, branching, done
408
+ var ctxTokens = [];
409
+ var correctTokens = [];
410
+ var incorrectTokens = [];
411
+ var ctxIdx = 0;
412
+ var branchIdx = 0;
413
+ var timer = null;
414
+ var loopTimer = null;
415
+ var TOKEN_DELAY = 120; // ms per token
416
+
417
+ /* ---------- TOKENIZE ---------- */
418
+ function tokenize(text) {
419
+ var raw = text.split(/(\s+)/);
420
+ var tokens = [];
421
+ for (var i = 0; i < raw.length; i++) {
422
+ if (raw[i] !== '' && !/^\s+$/.test(raw[i])) tokens.push(raw[i]);
423
+ }
424
+ return tokens;
425
+ }
426
+
427
+ /* ---------- HELPERS ---------- */
428
+ function el(tag, cls) {
429
+ var e = document.createElement(tag);
430
+ if (cls) e.className = cls;
431
+ return e;
432
+ }
433
+
434
+ function parseFeature(f) {
435
+ var m = f.match(/^(L\d+)\/(\d+)$/);
436
+ if (!m) return { layer: f, id: '' };
437
+ return { layer: m[1], id: m[2] };
438
+ }
439
+
440
+ function escapeHtml(s) {
441
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
442
+ }
443
+
444
+ function makeFeaturePill(feature, description) {
445
+ var parsed = parseFeature(feature);
446
+ var n = parseInt(parsed.layer.replace('L', ''), 10);
447
+ var colorClass = n <= 15 ? 'layer-blue' : 'layer-purple';
448
+ var url = 'https://neuronpedia.org/gemma-2-2b/' + n + '-gemmascope-res-16k/' + parsed.id;
449
+ return '<a class="be-feature-pill" href="' + url + '" target="_blank" rel="noopener" style="text-decoration:none;color:inherit;cursor:pointer">' +
450
+ '<span class="fp-layer ' + colorClass + '">' + parsed.layer + '</span>' +
451
+ '<span class="fp-id">' + parsed.id + '</span>' +
452
+ '<span class="fp-tooltip">' + escapeHtml(description.trim()) + '</span>' +
453
+ '</a>';
454
+ }
455
+
456
+ function addToken(container, text, colorClass) {
457
+ var tok = el('span', 'be-token ' + colorClass);
458
+ tok.textContent = text;
459
+ container.appendChild(tok);
460
+ requestAnimationFrame(function() {
461
+ requestAnimationFrame(function() { tok.classList.add('visible'); });
462
+ });
463
+ }
464
+
465
+ /* ---------- BUILD DOM ---------- */
466
+ container.innerHTML = '';
467
+
468
+ // Nav
469
+ var nav = el('div', 'be-nav');
470
+ var prevBtn = el('button', 'be-nav-btn'); prevBtn.textContent = '\u2190'; prevBtn.setAttribute('aria-label', 'Previous');
471
+ var playBtn = el('button', 'be-play-btn'); playBtn.innerHTML = '\u25B6 Play';
472
+ var navLabel = el('span', 'be-nav-label');
473
+ var nextBtn = el('button', 'be-nav-btn'); nextBtn.textContent = '\u2192'; nextBtn.setAttribute('aria-label', 'Next');
474
+ nav.appendChild(prevBtn);
475
+ nav.appendChild(playBtn);
476
+ nav.appendChild(navLabel);
477
+ nav.appendChild(nextBtn);
478
+ container.appendChild(nav);
479
+
480
+ // Question card
481
+ var questionCard = el('div', 'be-question-card');
482
+ var qLabel = el('div', 'be-question-label'); qLabel.textContent = 'GSM8K Question';
483
+ var qText = el('div', 'be-question-text');
484
+ questionCard.appendChild(qLabel);
485
+ questionCard.appendChild(qText);
486
+ container.appendChild(questionCard);
487
+
488
+ // Context card
489
+ var contextCard = el('div', 'be-context-card');
490
+ var cLabel = el('div', 'be-context-label'); cLabel.textContent = 'Shared Answer Prefix';
491
+ var cText = el('div', 'be-context-text');
492
+ contextCard.appendChild(cLabel);
493
+ contextCard.appendChild(cText);
494
+ container.appendChild(contextCard);
495
+
496
+ // Progress bar
497
+ var progressBar = el('div', 'be-progress');
498
+ var progressFill = el('div', 'be-progress-fill');
499
+ progressBar.appendChild(progressFill);
500
+ container.appendChild(progressBar);
501
+
502
+ // SVG tree
503
+ var treeWrap = el('div', 'be-tree-wrap');
504
+ container.appendChild(treeWrap);
505
+
506
+ // Outputs grid
507
+ var outputsGrid = el('div', 'be-outputs');
508
+
509
+ var correctCard = el('div', 'be-output-card correct');
510
+ var correctHeader = el('div', 'be-output-header');
511
+ var correctRow = el('div', 'be-output-tokens');
512
+ correctCard.appendChild(correctHeader);
513
+ correctCard.appendChild(correctRow);
514
+
515
+ var incorrectCard = el('div', 'be-output-card incorrect');
516
+ var incorrectHeader = el('div', 'be-output-header');
517
+ var incorrectRow = el('div', 'be-output-tokens');
518
+ incorrectCard.appendChild(incorrectHeader);
519
+ incorrectCard.appendChild(incorrectRow);
520
+
521
+ outputsGrid.appendChild(correctCard);
522
+ outputsGrid.appendChild(incorrectCard);
523
+ container.appendChild(outputsGrid);
524
+
525
+ /* ---------- D3 TREE ---------- */
526
+ var svgWidth = 520, svgHeight = 140;
527
+ var svg = d3.select(treeWrap)
528
+ .append('svg')
529
+ .attr('viewBox', '0 0 ' + svgWidth + ' ' + svgHeight)
530
+ .attr('width', '100%')
531
+ .attr('preserveAspectRatio', 'xMidYMid meet')
532
+ .style('max-width', svgWidth + 'px');
533
+
534
+ var defs = svg.append('defs');
535
+ var glowFilter = defs.append('filter').attr('id', 'be-glow');
536
+ glowFilter.append('feGaussianBlur').attr('stdDeviation', '3').attr('result', 'coloredBlur');
537
+ var feMerge = glowFilter.append('feMerge');
538
+ feMerge.append('feMergeNode').attr('in', 'coloredBlur');
539
+ feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
540
+
541
+ var nodes = {
542
+ context: { x: 60, y: 70 },
543
+ branch: { x: 220, y: 70 },
544
+ correct: { x: 440, y: 30 },
545
+ incorrect: { x: 440, y: 110 }
546
+ };
547
+
548
+ function bezier(from, to) {
549
+ var mx = (from.x + to.x) / 2;
550
+ return 'M' + from.x + ',' + from.y + ' C' + mx + ',' + from.y + ' ' + mx + ',' + to.y + ' ' + to.x + ',' + to.y;
551
+ }
552
+
553
+ // Trunk (dashed)
554
+ svg.append('path').attr('class', 'branch-path')
555
+ .attr('d', 'M' + nodes.context.x + ',' + nodes.context.y + ' L' + nodes.branch.x + ',' + nodes.branch.y)
556
+ .attr('stroke', 'var(--muted-color, #9ca3af)').attr('stroke-dasharray', '4,3');
557
+
558
+ var correctPath = svg.append('path').attr('class', 'branch-path')
559
+ .attr('d', bezier(nodes.branch, nodes.correct)).attr('stroke', '#10B981');
560
+ var incorrectPath = svg.append('path').attr('class', 'branch-path')
561
+ .attr('d', bezier(nodes.branch, nodes.incorrect)).attr('stroke', '#EF4444');
562
+
563
+ function animatePath(p) {
564
+ var len = p.node().getTotalLength();
565
+ p.attr('stroke-dasharray', len).attr('stroke-dashoffset', len)
566
+ .transition().duration(600).ease(d3.easeCubicOut).attr('stroke-dashoffset', 0);
567
+ }
568
+
569
+ // Context node
570
+ svg.append('circle').attr('cx', nodes.context.x).attr('cy', nodes.context.y).attr('r', 10)
571
+ .attr('fill', 'var(--surface-bg, #f3f4f6)').attr('stroke', 'var(--border-color, #d1d5db)').attr('stroke-width', 2);
572
+ svg.append('text').attr('class', 'node-label').attr('x', nodes.context.x).attr('y', nodes.context.y + 28)
573
+ .attr('text-anchor', 'middle').attr('fill', 'var(--muted-color, #6b7280)').text('Context');
574
+
575
+ // Branch node (glow)
576
+ svg.append('circle').attr('cx', nodes.branch.x).attr('cy', nodes.branch.y).attr('r', 12)
577
+ .attr('fill', '#fbbf24').attr('opacity', 0.3).attr('filter', 'url(#be-glow)');
578
+ svg.append('circle').attr('cx', nodes.branch.x).attr('cy', nodes.branch.y).attr('r', 8)
579
+ .attr('fill', '#fbbf24').attr('stroke', '#f59e0b').attr('stroke-width', 2);
580
+ svg.append('text').attr('class', 'node-label').attr('x', nodes.branch.x).attr('y', nodes.branch.y + 26)
581
+ .attr('text-anchor', 'middle').attr('fill', 'var(--muted-color, #6b7280)').text('SAE Feature');
582
+
583
+ // Correct endpoint
584
+ svg.append('circle').attr('cx', nodes.correct.x).attr('cy', nodes.correct.y).attr('r', 10)
585
+ .attr('fill', '#10B981').attr('opacity', 0.9);
586
+ var correctLabel = svg.append('text').attr('class', 'node-label')
587
+ .attr('x', nodes.correct.x + 16).attr('y', nodes.correct.y + 4)
588
+ .attr('fill', '#10B981').attr('font-weight', 600).attr('font-size', '11px');
589
+
590
+ // Incorrect endpoint
591
+ svg.append('circle').attr('cx', nodes.incorrect.x).attr('cy', nodes.incorrect.y).attr('r', 10)
592
+ .attr('fill', '#EF4444').attr('opacity', 0.9);
593
+ var incorrectLabel = svg.append('text').attr('class', 'node-label')
594
+ .attr('x', nodes.incorrect.x + 16).attr('y', nodes.incorrect.y + 4)
595
+ .attr('fill', '#EF4444').attr('font-weight', 600).attr('font-size', '11px');
596
+
597
+ /* ---------- LOAD EXAMPLE ---------- */
598
+ function loadExample(idx) {
599
+ var d = BRANCHES[idx];
600
+
601
+ // Nav
602
+ navLabel.textContent = 'Example ' + (idx + 1) + ' of ' + BRANCHES.length;
603
+ prevBtn.disabled = idx === 0;
604
+ nextBtn.disabled = idx === BRANCHES.length - 1;
605
+
606
+ // Question
607
+ if (d.question) {
608
+ qText.textContent = d.question;
609
+ questionCard.style.display = '';
610
+ } else {
611
+ questionCard.style.display = 'none';
612
+ }
613
+
614
+ // Tokenize
615
+ ctxTokens = tokenize(d.context);
616
+ correctTokens = tokenize(d.correct.output);
617
+ incorrectTokens = tokenize(d.incorrect.output);
618
+
619
+ // Tree labels
620
+ correctLabel.text(d.correct.feature);
621
+ incorrectLabel.text(d.incorrect.feature);
622
+
623
+ // Headers
624
+ correctHeader.innerHTML = '<span class="be-output-badge correct">' +
625
+ '<span class="badge-icon">\u2713</span><span class="badge-label">Correct</span></span> ' +
626
+ makeFeaturePill(d.correct.feature, d.correct.description);
627
+ incorrectHeader.innerHTML = '<span class="be-output-badge incorrect">' +
628
+ '<span class="badge-icon">\u2717</span><span class="badge-label">Incorrect</span></span> ' +
629
+ makeFeaturePill(d.incorrect.feature, d.incorrect.description);
630
+
631
+ resetVisuals();
632
+
633
+ // Update hero feature badges
634
+ var heroFeats = document.getElementById('hero-features');
635
+ if (heroFeats) {
636
+ function heroBadge(feat, desc) {
637
+ var parts = feat.split('/');
638
+ var n = parseInt(parts[0].replace('L',''), 10);
639
+ var url = 'https://neuronpedia.org/gemma-2-2b/' + n + '-gemmascope-res-16k/' + parts[1];
640
+ return '<a class="hero-feat-badge" href="' + url + '" target="_blank" rel="noopener">' +
641
+ '<span class="feat-id">' + feat + '</span>' +
642
+ '<span class="feat-desc">' + escapeHtml(desc.trim()) + '</span></a>';
643
+ }
644
+ heroFeats.innerHTML = heroBadge(d.correct.feature, d.correct.description) +
645
+ heroBadge(d.incorrect.feature, d.incorrect.description);
646
+ }
647
+ }
648
+
649
+ function resetVisuals() {
650
+ cText.innerHTML = '';
651
+ correctRow.innerHTML = '';
652
+ incorrectRow.innerHTML = '';
653
+ ctxIdx = 0;
654
+ branchIdx = 0;
655
+ treeWrap.classList.remove('visible');
656
+ outputsGrid.classList.remove('visible');
657
+ progressFill.style.width = '0%';
658
+ phase = 'idle';
659
+ }
660
+
661
+ /* ---------- ANIMATION ---------- */
662
+ function updateProgress() {
663
+ var total = ctxTokens.length + Math.max(correctTokens.length, incorrectTokens.length);
664
+ var current = ctxIdx + branchIdx;
665
+ progressFill.style.width = Math.min(100, (current / total) * 100) + '%';
666
+ }
667
+
668
+ function tick() {
669
+ if (!playing) return;
670
+
671
+ if (phase === 'context') {
672
+ if (ctxIdx < ctxTokens.length) {
673
+ addToken(cText, ctxTokens[ctxIdx], 'neutral');
674
+ ctxIdx++;
675
+ updateProgress();
676
+ timer = setTimeout(tick, TOKEN_DELAY);
677
+ } else {
678
+ // Show branch point
679
+ phase = 'branch-pause';
680
+ treeWrap.classList.add('visible');
681
+ animatePath(correctPath);
682
+ animatePath(incorrectPath);
683
+ // Pause then start branching
684
+ timer = setTimeout(function() {
685
+ if (!playing) return;
686
+ phase = 'branching';
687
+ outputsGrid.classList.add('visible');
688
+ tick();
689
+ }, 1200);
690
+ }
691
+ } else if (phase === 'branching') {
692
+ if (branchIdx < Math.max(correctTokens.length, incorrectTokens.length)) {
693
+ if (branchIdx < correctTokens.length) {
694
+ addToken(correctRow, correctTokens[branchIdx], 'tok-green');
695
+ }
696
+ if (branchIdx < incorrectTokens.length) {
697
+ addToken(incorrectRow, incorrectTokens[branchIdx], 'tok-red');
698
+ }
699
+ branchIdx++;
700
+ updateProgress();
701
+ timer = setTimeout(tick, TOKEN_DELAY);
702
+ } else {
703
+ // Done
704
+ phase = 'done';
705
+ progressFill.style.width = '100%';
706
+ playBtn.innerHTML = '\u25B6 Play';
707
+
708
+ // Auto-advance after 3s
709
+ loopTimer = setTimeout(function() {
710
+ currentIndex = (currentIndex + 1) % BRANCHES.length;
711
+ loadExample(currentIndex);
712
+ startPlay();
713
+ }, 3000);
714
+ }
715
+ }
716
+ }
717
+
718
+ function startPlay() {
719
+ stopTimers();
720
+ playing = true;
721
+ playBtn.innerHTML = 'Pause';
722
+
723
+ if (phase === 'done' || phase === 'idle') {
724
+ resetVisuals();
725
+ phase = 'context';
726
+ }
727
+ tick();
728
+ }
729
+
730
+ function pausePlay() {
731
+ playing = false;
732
+ stopTimers();
733
+ playBtn.innerHTML = '\u25B6 Play';
734
+ }
735
+
736
+ function stopTimers() {
737
+ clearTimeout(timer);
738
+ clearTimeout(loopTimer);
739
+ timer = null;
740
+ loopTimer = null;
741
+ }
742
+
743
+ /* ---------- INSTANT RENDER (for prev/next) ---------- */
744
+ function renderInstant() {
745
+ var d = BRANCHES[currentIndex];
746
+ resetVisuals();
747
+
748
+ // Show all context tokens instantly
749
+ for (var i = 0; i < ctxTokens.length; i++) {
750
+ var tok = el('span', 'be-token neutral visible');
751
+ tok.textContent = ctxTokens[i];
752
+ cText.appendChild(tok);
753
+ }
754
+ ctxIdx = ctxTokens.length;
755
+
756
+ // Show tree
757
+ treeWrap.classList.add('visible');
758
+
759
+ // Show all output tokens instantly
760
+ for (var j = 0; j < correctTokens.length; j++) {
761
+ var ct = el('span', 'be-token tok-green visible');
762
+ ct.textContent = correctTokens[j];
763
+ correctRow.appendChild(ct);
764
+ }
765
+ for (var k = 0; k < incorrectTokens.length; k++) {
766
+ var it = el('span', 'be-token tok-red visible');
767
+ it.textContent = incorrectTokens[k];
768
+ incorrectRow.appendChild(it);
769
+ }
770
+
771
+ outputsGrid.classList.add('visible');
772
+ progressFill.style.width = '100%';
773
+ phase = 'done';
774
+ }
775
+
776
+ /* ---------- EVENTS ---------- */
777
+ playBtn.addEventListener('click', function() {
778
+ if (playing) { pausePlay(); } else { startPlay(); }
779
+ });
780
+
781
+ prevBtn.addEventListener('click', function() {
782
+ pausePlay();
783
+ if (currentIndex > 0) {
784
+ currentIndex--;
785
+ loadExample(currentIndex);
786
+ renderInstant();
787
+ }
788
+ });
789
+
790
+ nextBtn.addEventListener('click', function() {
791
+ pausePlay();
792
+ if (currentIndex < BRANCHES.length - 1) {
793
+ currentIndex++;
794
+ loadExample(currentIndex);
795
+ renderInstant();
796
+ }
797
+ });
798
+
799
+ /* ---------- INIT ---------- */
800
+ loadExample(0);
801
+ renderInstant();
802
+ } // end mountBranchExplorer
803
+ }
804
+ })();
805
+ </script>
app/src/content/embeds/critic-trajectories.html ADDED
The diff for this file is too large to render. See raw diff
 
app/src/content/embeds/crl-flowchart-animated.html ADDED
@@ -0,0 +1,637 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="crl-flowchart-animated"></div>
2
+
3
+ <style>
4
+ .crl-flowchart-animated {
5
+ font-family: system-ui, -apple-system, sans-serif;
6
+ border: 1px solid var(--border-color, #e5e5e5);
7
+ border-radius: 16px;
8
+ padding: 24px;
9
+ background: var(--page-bg, #ffffff);
10
+ overflow: hidden;
11
+ position: relative;
12
+ }
13
+
14
+ .crl-flowchart-animated svg {
15
+ display: block;
16
+ width: 100%;
17
+ height: auto;
18
+ }
19
+
20
+ .crl-flowchart-animated .fc-node {
21
+ cursor: default;
22
+ transition: filter 0.2s ease;
23
+ }
24
+
25
+ .crl-flowchart-animated .fc-node:hover {
26
+ filter: brightness(1.08);
27
+ }
28
+
29
+ .crl-flowchart-animated .fc-node:hover .fc-box {
30
+ stroke-width: 2.5;
31
+ }
32
+
33
+ .crl-flowchart-animated .fc-label {
34
+ font-size: 11.5px;
35
+ font-weight: 600;
36
+ fill: var(--text-color, #1c1c1c);
37
+ pointer-events: none;
38
+ dominant-baseline: central;
39
+ text-anchor: middle;
40
+ }
41
+
42
+ .crl-flowchart-animated .fc-label.fc-label-light {
43
+ fill: #fafafa;
44
+ }
45
+
46
+ .crl-flowchart-animated .fc-sublabel {
47
+ font-size: 9px;
48
+ font-weight: 400;
49
+ fill: var(--muted-color, #737373);
50
+ pointer-events: none;
51
+ dominant-baseline: central;
52
+ text-anchor: middle;
53
+ }
54
+
55
+ .crl-flowchart-animated .fc-sublabel.fc-sublabel-light {
56
+ fill: #d4d4d4;
57
+ }
58
+
59
+ .crl-flowchart-animated .fc-arrow-forward {
60
+ stroke: var(--muted-color, #a3a3a3);
61
+ stroke-width: 1.8;
62
+ fill: none;
63
+ opacity: 0;
64
+ visibility: hidden;
65
+ }
66
+
67
+ .crl-flowchart-animated .fc-arrow-feedback {
68
+ stroke: #6366f1;
69
+ stroke-width: 1.8;
70
+ fill: none;
71
+ stroke-dasharray: 6, 4;
72
+ opacity: 0;
73
+ visibility: hidden;
74
+ }
75
+
76
+ .crl-flowchart-animated .fc-arrow-active {
77
+ visibility: visible;
78
+ animation: crl-arrow-fadein 0.5s ease both;
79
+ }
80
+
81
+ .crl-flowchart-animated .fc-arrow-feedback-active {
82
+ visibility: visible;
83
+ animation: crl-fb-fadein 0.4s ease both, crl-dash-flow 1.2s linear 0.4s infinite;
84
+ }
85
+
86
+ .crl-flowchart-animated .fc-node-hidden {
87
+ opacity: 0;
88
+ transform: translateY(8px);
89
+ }
90
+
91
+ @keyframes crl-node-reveal {
92
+ from { opacity: 0; transform: translateY(8px); }
93
+ to { opacity: 1; transform: translateY(0); }
94
+ }
95
+
96
+ .crl-flowchart-animated .fc-node-visible {
97
+ animation: crl-node-reveal 0.4s ease both;
98
+ }
99
+
100
+ .crl-flowchart-animated .fc-arrow-highlight {
101
+ stroke-width: 3;
102
+ stroke: #6366f1 !important;
103
+ }
104
+
105
+ .crl-flowchart-animated .fc-fo {
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ height: 100%;
110
+ font-size: 12px;
111
+ font-weight: 600;
112
+ text-align: center;
113
+ line-height: 1.2;
114
+ }
115
+ .crl-flowchart-animated .fc-fo-sub {
116
+ font-size: 9px;
117
+ font-weight: 400;
118
+ align-items: flex-start;
119
+ padding-top: 2px;
120
+ }
121
+ .crl-flowchart-animated .fc-fo .katex { font-size: inherit; }
122
+
123
+ .crl-flowchart-animated .fc-tooltip {
124
+ position: absolute;
125
+ background: var(--surface-bg, #fafafa);
126
+ border: 1px solid var(--border-color, #e5e5e5);
127
+ border-radius: 8px;
128
+ padding: 8px 12px;
129
+ font-size: 12px;
130
+ color: var(--text-color, #2e2e2e);
131
+ line-height: 1.5;
132
+ pointer-events: none;
133
+ opacity: 0;
134
+ transition: opacity 0.2s ease;
135
+ z-index: 10;
136
+ max-width: 240px;
137
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
138
+ white-space: normal;
139
+ }
140
+
141
+ .crl-flowchart-animated .fc-tooltip-visible {
142
+ opacity: 1;
143
+ }
144
+
145
+ .crl-flowchart-animated .fc-afm-box {
146
+ fill: none;
147
+ stroke: var(--muted-color, #a3a3a3);
148
+ stroke-dasharray: 4, 3;
149
+ stroke-width: 1.2;
150
+ rx: 6;
151
+ }
152
+
153
+ .crl-flowchart-animated .fc-afm-label {
154
+ font-size: 8.5px;
155
+ font-weight: 600;
156
+ fill: var(--muted-color, #737373);
157
+ pointer-events: none;
158
+ text-anchor: middle;
159
+ }
160
+
161
+ .crl-flowchart-animated .fc-section-label {
162
+ font-size: 9px;
163
+ font-weight: 700;
164
+ fill: var(--muted-color, #a3a3a3);
165
+ text-transform: uppercase;
166
+ letter-spacing: 1.2px;
167
+ pointer-events: none;
168
+ }
169
+
170
+ @keyframes crl-arrow-fadein {
171
+ from { opacity: 0; }
172
+ to { opacity: 1; }
173
+ }
174
+
175
+ @keyframes crl-fb-fadein {
176
+ from { opacity: 0; }
177
+ to { opacity: 0.85; }
178
+ }
179
+
180
+ @keyframes crl-dash-flow {
181
+ to { stroke-dashoffset: -20; }
182
+ }
183
+ </style>
184
+
185
+ <script>
186
+ (() => {
187
+ const bootstrap = () => {
188
+ const scriptEl = document.currentScript;
189
+ let container = scriptEl ? scriptEl.previousElementSibling : null;
190
+ if (!(container && container.classList && container.classList.contains('crl-flowchart-animated'))) {
191
+ const candidates = Array.from(document.querySelectorAll('.crl-flowchart-animated'))
192
+ .filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
193
+ container = candidates[candidates.length - 1] || null;
194
+ }
195
+ if (!container) return;
196
+ if (container.dataset) {
197
+ if (container.dataset.mounted === 'true') return;
198
+ container.dataset.mounted = 'true';
199
+ }
200
+
201
+ // --- Layout constants ---
202
+ const W = 960;
203
+ const H = 430;
204
+ const BOX_H = 44;
205
+ const BOX_RX = 12;
206
+ const ROW_Y = [68, 202, 340];
207
+
208
+ // Column positions (x centers) - evenly distributed with generous gaps
209
+ const COL = {
210
+ input: 62,
211
+ llm1: 215,
212
+ residual: 385,
213
+ sae: 545,
214
+ policy: 710,
215
+ featsel: 215,
216
+ steering: 420,
217
+ llm2: 630,
218
+ output: 815,
219
+ reward: 815,
220
+ critic: 630,
221
+ ppo: 420,
222
+ };
223
+
224
+ // Box widths
225
+ const BW = {
226
+ input: 86,
227
+ llm1: 148,
228
+ residual: 138,
229
+ sae: 118,
230
+ policy: 108,
231
+ featsel: 148,
232
+ steering: 210,
233
+ llm2: 148,
234
+ output: 96,
235
+ reward: 96,
236
+ critic: 108,
237
+ ppo: 118,
238
+ };
239
+
240
+ // Node definitions: id, label, sublabel, x, y, w, h, style
241
+ const nodes = [
242
+ // Row 1: Forward pass
243
+ { id: 'input', label: 'Input', sub: null, x: COL.input, y: ROW_Y[0], w: BW.input, style: 'subtle' },
244
+ { id: 'llm1', label: 'LLM Layers 1..l\u22121', sub: null, x: COL.llm1, y: ROW_Y[0], w: BW.llm1, style: 'llm', math: '\\text{LLM Layers } 1..l{-}1' },
245
+ { id: 'residual', label: 'Residual Stream', sub: 'x at layer l', x: COL.residual, y: ROW_Y[0], w: BW.residual, style: 'subtle', mathSub: '\\mathbf{x}_l' },
246
+ { id: 'sae', label: 'SAE Encoder', sub: 'sparse features', x: COL.sae, y: ROW_Y[0], w: BW.sae, style: 'sae' },
247
+ { id: 'policy', label: 'Policy \u03C0\u03B8', sub: '2-layer MLP', x: COL.policy, y: ROW_Y[0], w: BW.policy, style: 'agent', math: '\\text{Policy } \\pi_\\theta' },
248
+ // Row 2: Steering
249
+ { id: 'featsel', label: 'Feature Selection', sub: 'TopK(\u03BC, k=1)', x: COL.featsel, y: ROW_Y[1], w: BW.featsel, style: 'agent', mathSub: '\\text{TopK}(\\mu,\\, k{=}1)' },
250
+ { id: 'steering', label: 'x\u0303 = x + c\u00B7a\u209CW_dec', sub: 'steering vector', x: COL.steering, y: ROW_Y[1], w: BW.steering, style: 'key', math: '\\tilde{\\mathbf{x}} = \\mathbf{x} + c \\cdot \\mathbf{a}_t \\mathbf{W}_{\\text{dec}}' },
251
+ { id: 'llm2', label: 'LLM Layers l+1..L', sub: null, x: COL.llm2, y: ROW_Y[1], w: BW.llm2, style: 'llm', math: '\\text{LLM Layers } l{+}1..L' },
252
+ { id: 'output', label: 'Output', sub: 'generated text', x: COL.output, y: ROW_Y[1], w: BW.output, style: 'subtle' },
253
+ // Row 3: Feedback
254
+ { id: 'reward', label: 'Reward', sub: 'binary signal', x: COL.reward, y: ROW_Y[2], w: BW.reward, style: 'feedback' },
255
+ { id: 'critic', label: 'Critic V\u03C6', sub: 'advantage est.', x: COL.critic, y: ROW_Y[2], w: BW.critic, style: 'agent', math: '\\text{Critic } V_\\varphi' },
256
+ { id: 'ppo', label: 'PPO Update', sub: 'clipped objective', x: COL.ppo, y: ROW_Y[2], w: BW.ppo, style: 'agent' },
257
+ ];
258
+
259
+ // Edges: [sourceId, targetId, type]
260
+ // type: 'forward' or 'feedback'
261
+ const edges = [
262
+ // Row 1
263
+ ['input', 'llm1', 'forward'],
264
+ ['llm1', 'residual', 'forward'],
265
+ ['residual', 'sae', 'forward'],
266
+ ['sae', 'policy', 'forward'],
267
+ // Row 1 -> Row 2
268
+ ['policy', 'featsel', 'forward'],
269
+ // Row 2
270
+ ['featsel', 'steering', 'forward'],
271
+ ['steering', 'llm2', 'forward'],
272
+ ['llm2', 'output', 'forward'],
273
+ // Row 2 -> Row 3
274
+ ['output', 'reward', 'feedback'],
275
+ // Row 3
276
+ ['reward', 'critic', 'feedback'],
277
+ ['critic', 'ppo', 'feedback'],
278
+ // Row 3 -> Row 1 (loop back)
279
+ ['ppo', 'policy', 'feedback'],
280
+ ];
281
+
282
+ // Tooltip descriptions
283
+ const tooltips = {
284
+ input: 'Text prompt fed to the model',
285
+ llm1: 'Transformer layers before the intervention point',
286
+ residual: 'Hidden state x at layer l \u2014 the policy\'s observation',
287
+ sae: 'Sparse autoencoder decomposes x into interpretable features',
288
+ policy: '2-layer MLP selecting which feature to amplify',
289
+ featsel: 'TopK(\u03BC, k=1) \u2014 single feature chosen per token',
290
+ steering: 'x\u0303 = x + c\u00B7a\u209CW_dec \u2014 modified activation',
291
+ llm2: 'Remaining transformer layers process the steered activation',
292
+ output: 'Generated text (temperature=0 for determinism)',
293
+ reward: 'Binary signal: correct (1) or incorrect (0)',
294
+ critic: 'Value function estimating expected reward',
295
+ ppo: 'Policy gradient with clipped surrogate objective',
296
+ };
297
+
298
+ // Style configs
299
+ const styles = {
300
+ llm: { fill: '#1c1c1c', stroke: '#2e2e2e', textClass: 'fc-label fc-label-light', subClass: 'fc-sublabel fc-sublabel-light' },
301
+ sae: { fill: 'rgba(124,58,237,0.13)', stroke: '#7c3aed', textClass: 'fc-label', subClass: 'fc-sublabel' },
302
+ agent: { fill: 'rgba(79,70,229,0.12)', stroke: '#4f46e5', textClass: 'fc-label', subClass: 'fc-sublabel' },
303
+ key: { fill: 'rgba(79,70,229,0.08)', stroke: '#4f46e5', textClass: 'fc-label', subClass: 'fc-sublabel', strokeW: 2.5 },
304
+ subtle: { fill: 'var(--surface-bg, #f5f5f5)', stroke: 'var(--border-color, #d4d4d4)', textClass: 'fc-label', subClass: 'fc-sublabel' },
305
+ feedback: { fill: 'rgba(239,68,68,0.10)', stroke: '#ef4444', textClass: 'fc-label', subClass: 'fc-sublabel' },
306
+ };
307
+
308
+ // --- Build node lookup ---
309
+ const nodeMap = {};
310
+ nodes.forEach(n => { nodeMap[n.id] = n; });
311
+
312
+ // --- Helpers ---
313
+ const getPort = (node, side) => {
314
+ const hw = node.w / 2;
315
+ const hh = BOX_H / 2;
316
+ switch (side) {
317
+ case 'left': return { x: node.x - hw, y: node.y };
318
+ case 'right': return { x: node.x + hw, y: node.y };
319
+ case 'top': return { x: node.x, y: node.y - hh };
320
+ case 'bottom': return { x: node.x, y: node.y + hh };
321
+ }
322
+ };
323
+
324
+ const determineSides = (src, tgt) => {
325
+ const dx = tgt.x - src.x;
326
+ const dy = tgt.y - src.y;
327
+ // Mostly horizontal
328
+ if (Math.abs(dx) > Math.abs(dy) * 0.5 && dx > 0) return ['right', 'left'];
329
+ if (Math.abs(dx) > Math.abs(dy) * 0.5 && dx < 0) return ['left', 'right'];
330
+ // Mostly vertical
331
+ if (dy > 0) return ['bottom', 'top'];
332
+ return ['top', 'bottom'];
333
+ };
334
+
335
+ const buildPath = (srcNode, tgtNode) => {
336
+ const [srcSide, tgtSide] = determineSides(srcNode, tgtNode);
337
+ const p1 = getPort(srcNode, srcSide);
338
+ const p2 = getPort(tgtNode, tgtSide);
339
+
340
+ const dx = p2.x - p1.x;
341
+ const dy = p2.y - p1.y;
342
+
343
+ // Use cubic bezier for smooth curves
344
+ let c1, c2;
345
+ if (srcSide === 'right' && tgtSide === 'left') {
346
+ const mx = dx * 0.45;
347
+ c1 = { x: p1.x + mx, y: p1.y };
348
+ c2 = { x: p2.x - mx, y: p2.y };
349
+ } else if (srcSide === 'bottom' && tgtSide === 'top') {
350
+ const my = dy * 0.45;
351
+ c1 = { x: p1.x, y: p1.y + my };
352
+ c2 = { x: p2.x, y: p2.y - my };
353
+ } else if (srcSide === 'left' && tgtSide === 'right') {
354
+ const mx = Math.abs(dx) * 0.45;
355
+ c1 = { x: p1.x - mx, y: p1.y };
356
+ c2 = { x: p2.x + mx, y: p2.y };
357
+ } else if (srcSide === 'top' && tgtSide === 'bottom') {
358
+ const my = Math.abs(dy) * 0.45;
359
+ c1 = { x: p1.x, y: p1.y - my };
360
+ c2 = { x: p2.x, y: p2.y + my };
361
+ } else {
362
+ c1 = { x: p1.x + dx * 0.4, y: p1.y };
363
+ c2 = { x: p2.x - dx * 0.4, y: p2.y };
364
+ }
365
+
366
+ return `M${p1.x},${p1.y} C${c1.x},${c1.y} ${c2.x},${c2.y} ${p2.x},${p2.y}`;
367
+ };
368
+
369
+ // Special path for Policy -> Feature Selection (wrap from row 1 to row 2)
370
+ const buildPolicyToFeatsel = () => {
371
+ const src = nodeMap['policy'];
372
+ const tgt = nodeMap['featsel'];
373
+ const p1 = getPort(src, 'bottom');
374
+ const p2 = getPort(tgt, 'top');
375
+ const midY = (p1.y + p2.y) / 2;
376
+ return `M${p1.x},${p1.y} C${p1.x},${midY} ${p2.x},${midY} ${p2.x},${p2.y}`;
377
+ };
378
+
379
+ // Special path for PPO -> Policy (feedback loop: down from PPO, right along bottom, up right side, into Policy)
380
+ const buildFeedbackLoop = () => {
381
+ const src = nodeMap['ppo'];
382
+ const tgt = nodeMap['policy'];
383
+ const p1 = getPort(src, 'bottom');
384
+ const p2 = getPort(tgt, 'right');
385
+ const bottomY = H - 16; // bottom margin
386
+ const rightX = W - 40; // right margin
387
+ const r = 16; // corner radius
388
+ // Down from PPO, rounded corner right, along bottom, rounded corner up, up right side, rounded corner left into Policy
389
+ return `M${p1.x},${p1.y} L${p1.x},${bottomY - r} Q${p1.x},${bottomY} ${p1.x + r},${bottomY} L${rightX - r},${bottomY} Q${rightX},${bottomY} ${rightX},${bottomY - r} L${rightX},${p2.y + r} Q${rightX},${p2.y} ${rightX - r},${p2.y} L${p2.x},${p2.y}`;
390
+ };
391
+
392
+ // --- Build SVG ---
393
+ let svgContent = '';
394
+
395
+ // Defs: arrowheads
396
+ svgContent += `
397
+ <defs>
398
+ <marker id="crl-arrow-fwd" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto" markerUnits="strokeWidth">
399
+ <path d="M0,0.5 L6.5,3 L0,5.5" fill="none" stroke="var(--muted-color, #a3a3a3)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
400
+ </marker>
401
+ <marker id="crl-arrow-fb" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto" markerUnits="strokeWidth">
402
+ <path d="M0,0.5 L6.5,3 L0,5.5" fill="none" stroke="#6366f1" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
403
+ </marker>
404
+ </defs>
405
+ `;
406
+
407
+ // Section dividers and labels
408
+ const divY1 = (ROW_Y[0] + ROW_Y[1]) / 2 + 5;
409
+ const divY2 = (ROW_Y[1] + ROW_Y[2]) / 2 + 5;
410
+ svgContent += `
411
+ <line x1="16" y1="${divY1}" x2="${W - 16}" y2="${divY1}" stroke="var(--border-color, #e5e5e5)" stroke-width="0.8" stroke-dasharray="3,4" opacity="0.5" />
412
+ <line x1="16" y1="${divY2}" x2="${W - 16}" y2="${divY2}" stroke="var(--border-color, #e5e5e5)" stroke-width="0.8" stroke-dasharray="3,4" opacity="0.5" />
413
+ <text class="fc-section-label" x="16" y="${ROW_Y[0] - 28}">Forward Pass</text>
414
+ <text class="fc-section-label" x="16" y="${ROW_Y[1] - 28}">Steering</text>
415
+ <text class="fc-section-label" x="16" y="${ROW_Y[2] - 28}">Training Loop</text>
416
+ `;
417
+
418
+ // Edges (rendered first, behind nodes)
419
+ edges.forEach(([srcId, tgtId, type], idx) => {
420
+ const src = nodeMap[srcId];
421
+ const tgt = nodeMap[tgtId];
422
+ const isFeedback = type === 'feedback';
423
+ let d;
424
+
425
+ if (srcId === 'ppo' && tgtId === 'policy') {
426
+ d = buildFeedbackLoop();
427
+ } else if (srcId === 'policy' && tgtId === 'featsel') {
428
+ d = buildPolicyToFeatsel();
429
+ } else {
430
+ d = buildPath(src, tgt);
431
+ }
432
+
433
+ const cls = isFeedback ? 'fc-arrow-feedback' : 'fc-arrow-forward';
434
+ const marker = isFeedback ? 'url(#crl-arrow-fb)' : 'url(#crl-arrow-fwd)';
435
+
436
+ svgContent += `<path class="${cls}" d="${d}" data-marker="${marker}" data-edge-idx="${idx}" data-src="${srcId}" data-tgt="${tgtId}" />`;
437
+ });
438
+
439
+ // AFM dashed annotation around Feature Selection
440
+ const fsNode = nodeMap['featsel'];
441
+ const afmPad = 12;
442
+ svgContent += `
443
+ <rect class="fc-afm-box"
444
+ x="${fsNode.x - fsNode.w / 2 - afmPad}" y="${fsNode.y - BOX_H / 2 - afmPad}"
445
+ width="${fsNode.w + afmPad * 2}" height="${BOX_H + afmPad * 2 + 14}" />
446
+ <text class="fc-afm-label" x="${fsNode.x}" y="${fsNode.y + BOX_H / 2 + afmPad + 6}">AFM: mask expands as tokens generate</text>
447
+ `;
448
+
449
+ // Nodes
450
+ nodes.forEach(n => {
451
+ const s = styles[n.style];
452
+ const hw = n.w / 2;
453
+ const hh = BOX_H / 2;
454
+ const sw = s.strokeW || 1.8;
455
+
456
+ svgContent += `<g class="fc-node fc-node-hidden" data-node-id="${n.id}">`;
457
+ svgContent += `<rect class="fc-box" x="${n.x - hw}" y="${n.y - hh}" width="${n.w}" height="${BOX_H}" rx="${BOX_RX}" fill="${s.fill}" stroke="${s.stroke}" stroke-width="${sw}" />`;
458
+
459
+ // Determine text color for foreignObject
460
+ const isLight = n.style === 'llm';
461
+ const txtColor = isLight ? '#fff' : 'var(--text-color, #1f2937)';
462
+ const subColor = isLight ? 'rgba(255,255,255,0.7)' : 'var(--muted-color, #6b7280)';
463
+
464
+ if (n.math && n.sub) {
465
+ svgContent += `<foreignObject x="${n.x - hw}" y="${n.y - hh}" width="${n.w}" height="${hh}"><div xmlns="http://www.w3.org/1999/xhtml" class="fc-fo" data-tex="${n.math}" style="color:${txtColor}">${n.label}</div></foreignObject>`;
466
+ if (n.mathSub) {
467
+ svgContent += `<foreignObject x="${n.x - hw}" y="${n.y}" width="${n.w}" height="${hh}"><div xmlns="http://www.w3.org/1999/xhtml" class="fc-fo fc-fo-sub" data-tex="${n.mathSub}" style="color:${subColor}">${n.sub}</div></foreignObject>`;
468
+ } else {
469
+ svgContent += `<text class="${s.subClass}" x="${n.x}" y="${n.y + 10}" font-size="9">${n.sub}</text>`;
470
+ }
471
+ } else if (n.math) {
472
+ svgContent += `<foreignObject x="${n.x - hw}" y="${n.y - hh}" width="${n.w}" height="${BOX_H}"><div xmlns="http://www.w3.org/1999/xhtml" class="fc-fo" data-tex="${n.math}" style="color:${txtColor}">${n.label}</div></foreignObject>`;
473
+ } else if (n.sub) {
474
+ if (n.mathSub) {
475
+ svgContent += `<text class="${s.textClass}" x="${n.x}" y="${n.y - 6}">${n.label}</text>`;
476
+ svgContent += `<foreignObject x="${n.x - hw}" y="${n.y}" width="${n.w}" height="${hh}"><div xmlns="http://www.w3.org/1999/xhtml" class="fc-fo fc-fo-sub" data-tex="${n.mathSub}" style="color:${subColor}">${n.sub}</div></foreignObject>`;
477
+ } else {
478
+ svgContent += `<text class="${s.textClass}" x="${n.x}" y="${n.y - 6}">${n.label}</text>`;
479
+ svgContent += `<text class="${s.subClass}" x="${n.x}" y="${n.y + 10}" font-size="9">${n.sub}</text>`;
480
+ }
481
+ } else {
482
+ svgContent += `<text class="${s.textClass}" x="${n.x}" y="${n.y + 1}">${n.label}</text>`;
483
+ }
484
+
485
+ svgContent += `</g>`;
486
+ });
487
+
488
+ const svgEl = `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="overflow:visible;">${svgContent}</svg>`;
489
+
490
+ // Tooltip element
491
+ const tooltipEl = '<div class="fc-tooltip"></div>';
492
+
493
+ container.innerHTML = svgEl + tooltipEl;
494
+
495
+ // --- KaTeX render ---
496
+ function renderTex() {
497
+ if (typeof katex === 'undefined') return;
498
+ container.querySelectorAll('.fc-fo[data-tex]').forEach(function(el) {
499
+ try { el.innerHTML = katex.renderToString(el.dataset.tex, { throwOnError: false, displayMode: false }); } catch(e) {}
500
+ });
501
+ }
502
+ if (typeof katex !== 'undefined') renderTex();
503
+ else { var _ki = setInterval(function(){ if(typeof katex!=='undefined'){clearInterval(_ki);renderTex();}},300); setTimeout(function(){clearInterval(_ki);},8000); }
504
+
505
+ // --- Animations: IntersectionObserver ---
506
+ const svg = container.querySelector('svg');
507
+ const tooltip = container.querySelector('.fc-tooltip');
508
+ const allNodeEls = svg.querySelectorAll('.fc-node');
509
+ const allEdgeEls = svg.querySelectorAll('.fc-arrow-forward, .fc-arrow-feedback');
510
+
511
+ // Reveal order: nodes by appearance
512
+ const revealOrder = [
513
+ 'input', 'llm1', 'residual', 'sae', 'policy',
514
+ 'featsel', 'steering', 'llm2', 'output',
515
+ 'reward', 'critic', 'ppo'
516
+ ];
517
+
518
+ // Edge reveal: after both endpoints are visible
519
+ const edgeRevealAfter = {
520
+ 0: ['input', 'llm1'],
521
+ 1: ['llm1', 'residual'],
522
+ 2: ['residual', 'sae'],
523
+ 3: ['sae', 'policy'],
524
+ 4: ['policy', 'featsel'],
525
+ 5: ['featsel', 'steering'],
526
+ 6: ['steering', 'llm2'],
527
+ 7: ['llm2', 'output'],
528
+ 8: ['output', 'reward'],
529
+ 9: ['reward', 'critic'],
530
+ 10: ['critic', 'ppo'],
531
+ 11: ['ppo', 'policy'],
532
+ };
533
+
534
+ let revealed = new Set();
535
+ let animationTriggered = false;
536
+
537
+ const runRevealAnimation = () => {
538
+ if (animationTriggered) return;
539
+ animationTriggered = true;
540
+
541
+ revealOrder.forEach((nodeId, i) => {
542
+ setTimeout(() => {
543
+ const el = svg.querySelector(`[data-node-id="${nodeId}"]`);
544
+ if (el) {
545
+ el.classList.remove('fc-node-hidden');
546
+ el.classList.add('fc-node-visible');
547
+ }
548
+ revealed.add(nodeId);
549
+
550
+ // Reveal edges after a short delay so the node is visible first
551
+ setTimeout(function() {
552
+ allEdgeEls.forEach(function(edgeEl) {
553
+ var idx = parseInt(edgeEl.getAttribute('data-edge-idx'));
554
+ var deps = edgeRevealAfter[idx];
555
+ if (deps && deps.every(function(d) { return revealed.has(d); })) {
556
+ if (edgeEl.dataset.edgeRevealed) return;
557
+ edgeEl.dataset.edgeRevealed = '1';
558
+ var isFb = edgeEl.classList.contains('fc-arrow-feedback');
559
+ var mk = edgeEl.getAttribute('data-marker');
560
+ if (mk) edgeEl.setAttribute('marker-end', mk);
561
+ if (isFb) {
562
+ edgeEl.classList.add('fc-arrow-feedback-active');
563
+ } else {
564
+ edgeEl.classList.add('fc-arrow-active');
565
+ }
566
+ }
567
+ });
568
+ }, 200);
569
+ }, i * 280);
570
+ });
571
+ };
572
+
573
+ // IntersectionObserver
574
+ if ('IntersectionObserver' in window) {
575
+ const observer = new IntersectionObserver((entries) => {
576
+ entries.forEach(entry => {
577
+ if (entry.isIntersecting) {
578
+ runRevealAnimation();
579
+ observer.disconnect();
580
+ }
581
+ });
582
+ }, { threshold: 0.2 });
583
+ observer.observe(container);
584
+ } else {
585
+ // Fallback: reveal immediately
586
+ runRevealAnimation();
587
+ }
588
+
589
+ // --- Hover interactions ---
590
+ allNodeEls.forEach(nodeEl => {
591
+ const nodeId = nodeEl.getAttribute('data-node-id');
592
+
593
+ nodeEl.addEventListener('mouseenter', (e) => {
594
+ // Highlight connected edges
595
+ allEdgeEls.forEach(edgeEl => {
596
+ const src = edgeEl.getAttribute('data-src');
597
+ const tgt = edgeEl.getAttribute('data-tgt');
598
+ if (src === nodeId || tgt === nodeId) {
599
+ edgeEl.classList.add('fc-arrow-highlight');
600
+ }
601
+ });
602
+
603
+ // Show tooltip
604
+ if (tooltips[nodeId]) {
605
+ tooltip.textContent = tooltips[nodeId];
606
+ tooltip.classList.add('fc-tooltip-visible');
607
+
608
+ const rect = container.getBoundingClientRect();
609
+ const svgRect = svg.getBoundingClientRect();
610
+ const node = nodeMap[nodeId];
611
+ // Convert SVG coords to pixel coords
612
+ const scaleX = svgRect.width / W;
613
+ const scaleY = svgRect.height / H;
614
+ const px = svgRect.left - rect.left + node.x * scaleX;
615
+ const py = svgRect.top - rect.top + (node.y - BOX_H / 2) * scaleY - 8;
616
+
617
+ tooltip.style.left = Math.max(8, Math.min(px - 60, rect.width - 220)) + 'px';
618
+ tooltip.style.top = Math.max(8, py - tooltip.offsetHeight) + 'px';
619
+ }
620
+ });
621
+
622
+ nodeEl.addEventListener('mouseleave', () => {
623
+ allEdgeEls.forEach(edgeEl => {
624
+ edgeEl.classList.remove('fc-arrow-highlight');
625
+ });
626
+ tooltip.classList.remove('fc-tooltip-visible');
627
+ });
628
+ });
629
+ };
630
+
631
+ if (document.readyState === 'loading') {
632
+ document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
633
+ } else {
634
+ bootstrap();
635
+ }
636
+ })();
637
+ </script>
app/src/content/embeds/feature-frequency.html ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!--
2
+ Feature Selection Frequency (Zipf-like)
3
+
4
+ Horizontal bar chart showing the top-10 most frequently selected SAE features
5
+ by the CRL policy on GSM8K layer 20. Data from feature_analysis_20.json "success" category.
6
+ Feature IDs link to Neuronpedia.
7
+ -->
8
+ <div class="feature-frequency"></div>
9
+ <style>
10
+ .feature-frequency {
11
+ position: relative;
12
+ font-family: system-ui, -apple-system, sans-serif;
13
+ width: 100%;
14
+ }
15
+
16
+ .feature-frequency .chart-card {
17
+ background: var(--page-bg);
18
+ border: 1px solid var(--border-color);
19
+ border-radius: 16px;
20
+ padding: 24px;
21
+ }
22
+
23
+ .feature-frequency .chart-header {
24
+ display: flex;
25
+ justify-content: space-between;
26
+ align-items: baseline;
27
+ margin-bottom: 20px;
28
+ flex-wrap: wrap;
29
+ gap: 8px;
30
+ }
31
+
32
+ .feature-frequency .chart-title {
33
+ font-size: 15px;
34
+ font-weight: 700;
35
+ color: var(--text-color);
36
+ }
37
+
38
+ .feature-frequency .chart-subtitle {
39
+ font-size: 12px;
40
+ color: var(--muted-color);
41
+ font-weight: 500;
42
+ }
43
+
44
+ .feature-frequency svg {
45
+ display: block;
46
+ width: 100%;
47
+ height: auto;
48
+ overflow: visible;
49
+ }
50
+
51
+ .feature-frequency .axis path { stroke: none; }
52
+ .feature-frequency .axis line { stroke: var(--grid-color); }
53
+ .feature-frequency .axis text { fill: var(--tick-color); font-size: 11px; }
54
+ .feature-frequency .grid line { stroke: var(--grid-color); stroke-dasharray: 2, 3; }
55
+ .feature-frequency .grid path { stroke: none; }
56
+
57
+ .feature-frequency .bar-rect {
58
+ cursor: pointer;
59
+ transition: opacity 0.15s ease;
60
+ }
61
+ .feature-frequency .bar-rect:hover { opacity: 0.82; }
62
+
63
+ .feature-frequency .bar-count {
64
+ font-size: 11px;
65
+ font-weight: 600;
66
+ fill: var(--text-color);
67
+ pointer-events: none;
68
+ }
69
+
70
+ .feature-frequency .y-label {
71
+ font-size: 11px;
72
+ fill: var(--text-color);
73
+ cursor: pointer;
74
+ text-decoration: none;
75
+ }
76
+ .feature-frequency .y-label:hover {
77
+ fill: #4F46E5;
78
+ text-decoration: underline;
79
+ }
80
+
81
+ .feature-frequency .y-desc {
82
+ font-size: 10px;
83
+ fill: var(--muted-color);
84
+ pointer-events: none;
85
+ }
86
+
87
+ .feature-frequency .axis-label {
88
+ fill: var(--text-color);
89
+ font-size: 12px;
90
+ font-weight: 600;
91
+ }
92
+
93
+ .feature-frequency .d3-tooltip {
94
+ position: absolute;
95
+ top: 0;
96
+ left: 0;
97
+ transform: translate(-9999px, -9999px);
98
+ pointer-events: none;
99
+ padding: 10px 14px;
100
+ border-radius: 12px;
101
+ font-size: 12px;
102
+ line-height: 1.5;
103
+ border: 1px solid var(--border-color);
104
+ background: var(--surface-bg);
105
+ color: var(--text-color);
106
+ box-shadow: 0 8px 32px rgba(0,0,0,.22), 0 2px 8px rgba(0,0,0,.10);
107
+ backdrop-filter: saturate(1.12) blur(8px);
108
+ -webkit-backdrop-filter: saturate(1.12) blur(8px);
109
+ opacity: 0;
110
+ transition: opacity 0.15s ease;
111
+ z-index: 20;
112
+ max-width: 260px;
113
+ }
114
+
115
+ .feature-frequency .entropy-badge {
116
+ display: inline-flex;
117
+ align-items: center;
118
+ gap: 5px;
119
+ padding: 4px 12px;
120
+ border-radius: 999px;
121
+ border: 1px solid var(--border-color);
122
+ font-size: 12px;
123
+ font-weight: 600;
124
+ color: var(--text-color);
125
+ background: var(--surface-bg, var(--page-bg));
126
+ }
127
+
128
+ .feature-frequency .entropy-badge .dot {
129
+ width: 8px;
130
+ height: 8px;
131
+ border-radius: 50%;
132
+ background: #4F46E5;
133
+ }
134
+
135
+ @media (max-width: 600px) {
136
+ .feature-frequency .chart-card {
137
+ padding: 14px;
138
+ }
139
+ .feature-frequency .y-label {
140
+ font-size: 10px;
141
+ }
142
+ .feature-frequency .y-desc {
143
+ font-size: 9px;
144
+ }
145
+ }
146
+ </style>
147
+ <script>
148
+ (function() {
149
+ var ensureD3 = function(cb) {
150
+ if (window.d3 && typeof window.d3.select === 'function') return cb();
151
+ var s = document.getElementById('d3-cdn-script');
152
+ if (!s) {
153
+ s = document.createElement('script');
154
+ s.id = 'd3-cdn-script';
155
+ s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
156
+ document.head.appendChild(s);
157
+ }
158
+ var onReady = function() {
159
+ if (window.d3 && typeof window.d3.select === 'function') cb();
160
+ };
161
+ s.addEventListener('load', onReady, { once: true });
162
+ if (window.d3) onReady();
163
+ };
164
+
165
+ var bootstrap = function() {
166
+ var candidates = Array.from(document.querySelectorAll('.feature-frequency'))
167
+ .filter(function(el) { return !el.dataset.mounted; });
168
+ var container = candidates[candidates.length - 1];
169
+ if (!container) return;
170
+ container.dataset.mounted = 'true';
171
+ container.style.position = 'relative';
172
+
173
+ var d3sel = window.d3;
174
+
175
+ /* ── Data (real from feature_analysis_20.json "success" category) ── */
176
+ var data = [
177
+ {rank: 1, feature: 2198, count: 699, label: "summaries, reflections, or clarifications"},
178
+ {rank: 2, feature: 6787, count: 644, label: "cellular biology and histology terms"},
179
+ {rank: 3, feature: 5890, count: 613, label: "technical terms and specifications"},
180
+ {rank: 4, feature: 6512, count: 518, label: "instructions and steps for processes"},
181
+ {rank: 5, feature: 14802, count: 473, label: "code structures, functions, methods"},
182
+ {rank: 6, feature: 16326, count: 452, label: "comparative measurements or evaluations"},
183
+ {rank: 7, feature: 8587, count: 439, label: "procedural language, claims and benefits"},
184
+ {rank: 8, feature: 9488, count: 425, label: "API functions and documentation"},
185
+ {rank: 9, feature: 5423, count: 409, label: "parentheses and numerical data"},
186
+ {rank: 10, feature: 9276, count: 399, label: "punctuation and conversational cues"}
187
+ ];
188
+
189
+ var totalCount = data.reduce(function(s, d) { return s + d.count; }, 0);
190
+
191
+ /* ── Tooltip ── */
192
+ var tip = document.createElement('div');
193
+ tip.className = 'd3-tooltip';
194
+ var tipInner = document.createElement('div');
195
+ tip.appendChild(tipInner);
196
+ container.appendChild(tip);
197
+
198
+ function showTip(event, html) {
199
+ tipInner.innerHTML = html;
200
+ var coords = d3sel.pointer(event, container);
201
+ var tipRect = tip.getBoundingClientRect();
202
+ var contW = container.getBoundingClientRect().width;
203
+ var contH = container.getBoundingClientRect().height;
204
+ var tx = coords[0] + 14;
205
+ var ty = coords[1] - 10;
206
+ if (tx + (tipRect.width || 200) > contW - 10) tx = coords[0] - (tipRect.width || 200) - 14;
207
+ if (ty + (tipRect.height || 80) > contH - 10) ty = coords[1] - (tipRect.height || 80) - 10;
208
+ if (ty < 0) ty = coords[1] + 20;
209
+ tip.style.opacity = '1';
210
+ tip.style.transform = 'translate(' + Math.round(tx) + 'px, ' + Math.round(ty) + 'px)';
211
+ }
212
+ function hideTip() {
213
+ tip.style.opacity = '0';
214
+ tip.style.transform = 'translate(-9999px, -9999px)';
215
+ }
216
+
217
+ /* ── Card ── */
218
+ var card = document.createElement('div');
219
+ card.className = 'chart-card';
220
+ container.appendChild(card);
221
+
222
+ /* Header */
223
+ var header = document.createElement('div');
224
+ header.className = 'chart-header';
225
+ header.innerHTML =
226
+ '<div>' +
227
+ '<div class="chart-title">Feature Selection Frequency</div>' +
228
+ '<div class="chart-subtitle">GSM8K \u00b7 Layer 20 \u00b7 Top-10 most selected SAE features</div>' +
229
+ '</div>' +
230
+ '<div class="entropy-badge"><span class="dot"></span>Diversity H = 6.695</div>';
231
+ card.appendChild(header);
232
+
233
+ /* SVG container */
234
+ var svgWrap = document.createElement('div');
235
+ svgWrap.style.position = 'relative';
236
+ card.appendChild(svgWrap);
237
+
238
+ var svg = d3sel.select(svgWrap).append('svg');
239
+ var defs = svg.append('defs');
240
+
241
+ /* Gradient definition */
242
+ var gradId = 'ff-grad-' + Math.random().toString(36).slice(2);
243
+ var grad = defs.append('linearGradient')
244
+ .attr('id', gradId)
245
+ .attr('x1', '0%').attr('y1', '0%')
246
+ .attr('x2', '0%').attr('y2', '100%');
247
+ grad.append('stop').attr('offset', '0%').attr('stop-color', '#4F46E5');
248
+ grad.append('stop').attr('offset', '100%').attr('stop-color', '#a3a3a3');
249
+
250
+ var gRoot = svg.append('g');
251
+ var gGrid = gRoot.append('g').attr('class', 'grid');
252
+ var gBars = gRoot.append('g').attr('class', 'bars');
253
+ var gAxes = gRoot.append('g').attr('class', 'axes');
254
+
255
+ var hasAnimated = false;
256
+
257
+ /* ── Color interpolation per rank ── */
258
+ function barColor(rank) {
259
+ var t = (rank - 1) / (data.length - 1);
260
+ return d3sel.interpolateRgb('#4F46E5', '#a3a3a3')(t);
261
+ }
262
+
263
+ /* ── Neuronpedia URL ── */
264
+ function neuronpediaUrl(featureId) {
265
+ return 'https://neuronpedia.org/gemma-2-2b/20-gemmascope-res-16k/' + featureId;
266
+ }
267
+
268
+ /* ── Render ── */
269
+ function render(animate) {
270
+ var containerWidth = container.clientWidth || 700;
271
+ var totalWidth = Math.max(340, Math.round(containerWidth));
272
+
273
+ /* Responsive margin: narrower left margin on mobile */
274
+ var isNarrow = totalWidth < 500;
275
+ var margin = {
276
+ top: 8,
277
+ right: 48,
278
+ bottom: 40,
279
+ left: isNarrow ? 130 : 200
280
+ };
281
+ var innerWidth = totalWidth - margin.left - margin.right;
282
+ var barHeight = isNarrow ? 22 : 26;
283
+ var barPadding = isNarrow ? 4 : 6;
284
+ var totalBarArea = data.length * (barHeight + barPadding) - barPadding;
285
+ var totalHeight = margin.top + totalBarArea + margin.bottom;
286
+
287
+ svg.attr('viewBox', '0 0 ' + totalWidth + ' ' + totalHeight)
288
+ .attr('preserveAspectRatio', 'xMidYMid meet');
289
+
290
+ gRoot.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
291
+
292
+ /* Scales */
293
+ var xMax = d3sel.max(data, function(d) { return d.count; });
294
+ var xScale = d3sel.scaleLinear()
295
+ .domain([0, xMax * 1.08])
296
+ .range([0, innerWidth]);
297
+
298
+ var yScale = d3sel.scaleBand()
299
+ .domain(data.map(function(d) { return d.rank; }))
300
+ .range([0, totalBarArea])
301
+ .paddingInner(barPadding / (barHeight + barPadding))
302
+ .paddingOuter(0);
303
+
304
+ /* Grid lines */
305
+ gGrid.selectAll('*').remove();
306
+ gGrid.append('g')
307
+ .call(d3sel.axisBottom(xScale)
308
+ .ticks(isNarrow ? 4 : 6)
309
+ .tickSize(totalBarArea)
310
+ .tickFormat('')
311
+ )
312
+ .call(function(g) { g.select('.domain').remove(); });
313
+
314
+ /* ── Bars ── */
315
+ var barGroups = gBars.selectAll('g.bar-group')
316
+ .data(data, function(d) { return d.rank; });
317
+
318
+ var barEnter = barGroups.enter().append('g').attr('class', 'bar-group');
319
+ barEnter.append('rect').attr('class', 'bar-rect');
320
+ barEnter.append('text').attr('class', 'bar-count');
321
+
322
+ var barMerge = barEnter.merge(barGroups);
323
+
324
+ barMerge.attr('transform', function(d) {
325
+ return 'translate(0,' + yScale(d.rank) + ')';
326
+ });
327
+
328
+ var rects = barMerge.select('.bar-rect')
329
+ .attr('y', 0)
330
+ .attr('height', yScale.bandwidth())
331
+ .attr('rx', 4)
332
+ .attr('ry', 4)
333
+ .attr('fill', function(d) { return barColor(d.rank); });
334
+
335
+ if (animate) {
336
+ rects
337
+ .attr('x', 0)
338
+ .attr('width', 0)
339
+ .transition()
340
+ .duration(800)
341
+ .delay(function(d, i) { return i * 40; })
342
+ .ease(d3sel.easeCubicOut)
343
+ .attr('width', function(d) { return xScale(d.count); });
344
+ } else {
345
+ rects
346
+ .attr('x', 0)
347
+ .attr('width', function(d) { return xScale(d.count); });
348
+ }
349
+
350
+ /* Hover */
351
+ rects
352
+ .on('mousemove', function(event, d) {
353
+ var pct = ((d.count / totalCount) * 100).toFixed(1);
354
+ var html =
355
+ '<div style="font-weight:700;margin-bottom:2px;">F' + d.feature + '</div>' +
356
+ '<div style="font-size:11px;color:var(--muted-color);margin-bottom:6px;">' + d.label + '</div>' +
357
+ '<div style="display:flex;gap:16px;">' +
358
+ '<div><span style="font-size:10px;color:var(--muted-color);">Count</span><br><strong style="font-size:16px;">' + d.count + '</strong></div>' +
359
+ '<div><span style="font-size:10px;color:var(--muted-color);">Share</span><br><strong style="font-size:16px;">' + pct + '%</strong></div>' +
360
+ '<div><span style="font-size:10px;color:var(--muted-color);">Rank</span><br><strong style="font-size:16px;">#' + d.rank + '</strong></div>' +
361
+ '</div>';
362
+ showTip(event, html);
363
+ })
364
+ .on('mouseleave', function() { hideTip(); })
365
+ .on('click', function(event, d) {
366
+ window.open(neuronpediaUrl(d.feature), '_blank', 'noopener');
367
+ });
368
+
369
+ /* Count labels at end of bar */
370
+ var countLabels = barMerge.select('.bar-count')
371
+ .attr('y', yScale.bandwidth() / 2)
372
+ .attr('dy', '0.35em')
373
+ .text(function(d) { return d.count; });
374
+
375
+ if (animate) {
376
+ countLabels
377
+ .attr('x', 4)
378
+ .attr('opacity', 0)
379
+ .transition()
380
+ .duration(400)
381
+ .delay(function(d, i) { return i * 40 + 600; })
382
+ .attr('x', function(d) { return xScale(d.count) + 6; })
383
+ .attr('opacity', 1);
384
+ } else {
385
+ countLabels
386
+ .attr('x', function(d) { return xScale(d.count) + 6; })
387
+ .attr('opacity', 1);
388
+ }
389
+
390
+ barGroups.exit().remove();
391
+
392
+ /* ── Y-axis labels (feature IDs + descriptions) ── */
393
+ gAxes.selectAll('*').remove();
394
+
395
+ data.forEach(function(d) {
396
+ var yPos = yScale(d.rank) + yScale.bandwidth() / 2;
397
+
398
+ /* Feature ID as clickable link */
399
+ var link = gAxes.append('a')
400
+ .attr('href', neuronpediaUrl(d.feature))
401
+ .attr('target', '_blank')
402
+ .attr('rel', 'noopener');
403
+
404
+ link.append('text')
405
+ .attr('class', 'y-label')
406
+ .attr('x', -8)
407
+ .attr('y', yPos)
408
+ .attr('dy', isNarrow ? '0.35em' : '-0.15em')
409
+ .attr('text-anchor', 'end')
410
+ .attr('font-weight', 600)
411
+ .text('F' + d.feature);
412
+
413
+ /* Description below feature ID (only on wider viewports) */
414
+ if (!isNarrow) {
415
+ gAxes.append('text')
416
+ .attr('class', 'y-desc')
417
+ .attr('x', -8)
418
+ .attr('y', yPos)
419
+ .attr('dy', '1.0em')
420
+ .attr('text-anchor', 'end')
421
+ .text(d.label);
422
+ }
423
+ });
424
+
425
+ /* X-axis */
426
+ gAxes.append('g')
427
+ .attr('class', 'axis x-axis')
428
+ .attr('transform', 'translate(0,' + totalBarArea + ')')
429
+ .call(d3sel.axisBottom(xScale).ticks(isNarrow ? 4 : 6));
430
+
431
+ gAxes.append('text')
432
+ .attr('class', 'axis-label')
433
+ .attr('text-anchor', 'middle')
434
+ .attr('x', innerWidth / 2)
435
+ .attr('y', totalBarArea + 34)
436
+ .text('Selection Count');
437
+ }
438
+
439
+ /* ── Initial render (no animation) then observe for scroll-in ── */
440
+ render(false);
441
+
442
+ var obs = new IntersectionObserver(function(entries) {
443
+ entries.forEach(function(entry) {
444
+ if (entry.isIntersecting && !hasAnimated) {
445
+ hasAnimated = true;
446
+ render(true);
447
+ obs.disconnect();
448
+ }
449
+ });
450
+ }, { threshold: 0.15 });
451
+ obs.observe(container);
452
+
453
+ /* Resize */
454
+ if (window.ResizeObserver) {
455
+ new ResizeObserver(function() {
456
+ if (hasAnimated) render(false);
457
+ }).observe(container);
458
+ }
459
+ };
460
+
461
+ if (document.readyState === 'loading') {
462
+ document.addEventListener('DOMContentLoaded', function() { ensureD3(bootstrap); }, { once: true });
463
+ } else {
464
+ ensureD3(bootstrap);
465
+ }
466
+ })();
467
+ </script>
app/src/content/embeds/flowchart-svg.svg ADDED
app/src/content/embeds/layer-heatmap.html ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!--
2
+ Layer Γ— Coefficient Sensitivity β€” Multi-line Chart
3
+ Shows accuracy across layers for different steering coefficients.
4
+ Data: real experiment results from DGX (compare_layers.jsonl etc.)
5
+ -->
6
+ <div class="layer-sensitivity"></div>
7
+ <style>
8
+ .layer-sensitivity {
9
+ font-family: var(--default-font-family, system-ui, -apple-system, sans-serif);
10
+ position: relative;
11
+ max-width: 760px;
12
+ margin: 0 auto;
13
+ }
14
+ .layer-sensitivity .ls-tabs {
15
+ display: flex;
16
+ gap: 4px;
17
+ margin-bottom: 8px;
18
+ flex-wrap: wrap;
19
+ }
20
+ .layer-sensitivity .ls-tab {
21
+ padding: 6px 14px;
22
+ border-radius: 8px;
23
+ border: 1.5px solid var(--border-color, rgba(0,0,0,0.12));
24
+ background: transparent;
25
+ color: var(--muted-color, #666);
26
+ font-size: 13px;
27
+ font-weight: 500;
28
+ cursor: pointer;
29
+ transition: all 0.2s;
30
+ font-family: inherit;
31
+ }
32
+ .layer-sensitivity .ls-tab:hover {
33
+ border-color: var(--text-color, #333);
34
+ color: var(--text-color, #333);
35
+ }
36
+ .layer-sensitivity .ls-tab.active {
37
+ background: var(--text-color, #222);
38
+ color: var(--page-bg, #fff);
39
+ border-color: var(--text-color, #222);
40
+ }
41
+ .layer-sensitivity .ls-mode-row {
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 8px;
45
+ margin-bottom: 12px;
46
+ }
47
+ .layer-sensitivity .ls-mode-btn {
48
+ padding: 4px 12px;
49
+ border-radius: 6px;
50
+ border: 1px solid var(--border-color, rgba(0,0,0,0.12));
51
+ background: transparent;
52
+ color: var(--muted-color, #888);
53
+ font-size: 12px;
54
+ font-weight: 500;
55
+ cursor: pointer;
56
+ transition: all 0.2s;
57
+ font-family: inherit;
58
+ }
59
+ .layer-sensitivity .ls-mode-btn.active {
60
+ background: oklch(from var(--text-color, #222) l c h / 0.12);
61
+ color: var(--text-color, #222);
62
+ border-color: var(--text-color, #222);
63
+ font-weight: 600;
64
+ }
65
+ .layer-sensitivity .ls-chart-wrap {
66
+ position: relative;
67
+ }
68
+ .layer-sensitivity .ls-legend {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 12px;
72
+ margin-top: 8px;
73
+ flex-wrap: wrap;
74
+ font-size: 12px;
75
+ color: var(--muted-color, #888);
76
+ }
77
+ .layer-sensitivity .ls-legend-item {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: 4px;
81
+ cursor: pointer;
82
+ transition: opacity 0.2s;
83
+ }
84
+ .layer-sensitivity .ls-legend-item.dimmed {
85
+ opacity: 0.25;
86
+ }
87
+ .layer-sensitivity .ls-legend-swatch {
88
+ width: 18px;
89
+ height: 3px;
90
+ border-radius: 2px;
91
+ }
92
+ .layer-sensitivity .ls-legend-baseline {
93
+ display: inline-flex;
94
+ align-items: center;
95
+ gap: 4px;
96
+ margin-left: 8px;
97
+ }
98
+ .layer-sensitivity .ls-legend-dash {
99
+ width: 18px;
100
+ height: 0;
101
+ border-top: 2px dashed var(--muted-color, #aaa);
102
+ }
103
+ .layer-sensitivity .ls-tooltip {
104
+ position: absolute;
105
+ pointer-events: none;
106
+ padding: 8px 12px;
107
+ border-radius: 10px;
108
+ font-size: 12px;
109
+ line-height: 1.5;
110
+ background: var(--page-bg, #fff);
111
+ border: 1px solid var(--border-color, rgba(0,0,0,0.12));
112
+ box-shadow: 0 4px 16px rgba(0,0,0,0.12);
113
+ opacity: 0;
114
+ transition: opacity 0.12s;
115
+ z-index: 10;
116
+ white-space: nowrap;
117
+ }
118
+ </style>
119
+ <script>
120
+ (function() {
121
+ var root = document.currentScript ? document.currentScript.parentElement.querySelector('.layer-sensitivity') : document.querySelector('.layer-sensitivity');
122
+ if (!root || root.dataset.mounted) return;
123
+ root.dataset.mounted = 'true';
124
+
125
+ /* ── Inline data from DGX experiments ── */
126
+ var DATA = {"mmlu":{"layer":{"10":{"0":0.484119,"1":0.471799,"2":0.444666,"3":0.45777,"4":0.481555,"5":0.48419,"6":0.480487,"7":0.457841,"8":0.441461,"9":0.461259,"10":0.515382,"11":0.523857,"12":0.521721,"13":0.523715,"14":0.540593,"15":0.53867,"16":0.540949,"17":0.54629,"18":0.541874,"19":0.546432,"20":0.5475,"21":0.547073,"22":0.539097,"23":0.549708,"24":0.551702,"25":0.542943},"20":{"0":0.480986,"1":0.474149,"2":0.445378,"3":0.463538,"4":0.476428,"5":0.482125,"6":0.476428,"7":0.455989,"8":0.440179,"9":0.463538,"10":0.514528,"11":0.524213,"12":0.525139,"13":0.521222,"14":0.538242,"15":0.536177,"16":0.543085,"17":0.545364,"18":0.539239,"19":0.544937,"20":0.547358,"21":0.547572,"22":0.537886,"23":0.550919,"24":0.550064,"25":0.543726},"30":{"0":0.47949,"1":0.457413,"2":0.430281,"3":0.45606,"4":0.479348,"5":0.485615,"6":0.472226,"7":0.448796,"8":0.441248,"9":0.46425,"10":0.515382,"11":0.521293,"12":0.519655,"13":0.520011,"14":0.539595,"15":0.537245,"16":0.537601,"17":0.543655,"18":0.538527,"19":0.546931,"20":0.547287,"21":0.546717,"22":0.537886,"23":0.549993,"24":0.551987,"25":0.543227},"40":{"0":0.471656,"1":0.420097,"2":0.443883,"3":0.438043,"4":0.469449,"5":0.468096,"6":0.469662,"7":0.450007,"8":0.448156,"9":0.46425,"10":0.512605,"11":0.52229,"12":0.51759,"13":0.518658,"14":0.536391,"15":0.536462,"16":0.540593,"17":0.544438,"18":0.53632,"19":0.545008,"20":0.547572,"21":0.546646,"22":0.537958,"23":0.549708,"24":0.552699,"25":0.543299},"50":{"0":0.463039,"1":0.486683,"2":0.432916,"3":0.439254,"4":0.457983,"5":0.464749,"6":0.459977,"7":0.440464,"8":0.448868,"9":0.469164,"10":0.513175,"11":0.518872,"12":0.509614,"13":0.51581,"14":0.535251,"15":0.534895,"16":0.539952,"17":0.543014,"18":0.535679,"19":0.543085,"20":0.547144,"21":0.547002,"22":0.538955,"23":0.549566,"24":0.552557,"25":0.542943},"60":{"0":0.462541,"1":0.452215,"2":0.43199,"3":0.438043,"4":0.439182,"5":0.452713,"6":0.433556,"7":0.431562,"8":0.454779,"9":0.469662,"10":0.506481,"11":0.512961,"12":0.511822,"13":0.513673,"14":0.529198,"15":0.53226,"16":0.535679,"17":0.543156,"18":0.533044,"19":0.544082,"20":0.546859,"21":0.544723,"22":0.538598,"23":0.549637,"24":0.552628,"25":0.542729},"70":{"0":0.459764,"1":0.333571,"2":0.425367,"3":0.422233,"4":0.439966,"5":0.437901,"6":0.43142,"7":0.430922,"8":0.434126,"9":0.473722,"10":0.507264,"11":0.512961,"12":0.503347,"13":0.508332,"14":0.524356,"15":0.530124,"16":0.531477,"17":0.54045,"18":0.529839,"19":0.5428,"20":0.546503,"21":0.544509,"22":0.537103,"23":0.550491,"24":0.552485,"25":0.543085},"80":{"0":0.445877,"1":0.356359,"2":0.438399,"3":0.371101,"4":0.413759,"5":0.393391,"6":0.436049,"7":0.432417,"8":0.441248,"9":0.456345,"10":0.494588,"11":0.50235,"12":0.495585,"13":0.500641,"14":0.521792,"15":0.525709,"16":0.530053,"17":0.539239,"18":0.530551,"19":0.540165,"20":0.545649,"21":0.544296,"22":0.536818,"23":0.54921,"24":0.551702,"25":0.542017},"90":{"0":0.432061,"1":0.474434,"2":0.360063,"3":0.399587,"4":0.410483,"5":0.423302,"6":0.408987,"7":0.420667,"8":0.431491,"9":0.453639,"10":0.494659,"11":0.488534,"12":0.484333,"13":0.489959,"14":0.516379,"15":0.527774,"16":0.532403,"17":0.535394,"18":0.528842,"19":0.539168,"20":0.543726,"21":0.544153,"22":0.537958,"23":0.548925,"24":0.550919,"25":0.540521},"100":{"0":0.43085,"1":0.390044,"2":0.395955,"3":0.351802,"4":0.363979,"5":0.394459,"6":0.400798,"7":0.404287,"8":0.42907,"9":0.453354,"10":0.472725,"11":0.473223,"12":0.465033,"13":0.475787,"14":0.501852,"15":0.520367,"16":0.523287,"17":0.53575,"18":0.524854,"19":0.541874,"20":0.543441,"21":0.54223,"22":0.535536,"23":0.54864,"24":0.549138,"25":0.542159}}},"mmlu_const":{"layer":{"30":{"0":0.5315,"1":0.5293,"2":0.5244,"3":0.5247,"4":0.5372,"5":0.5356,"6":0.5307,"7":0.5251,"8":0.5253,"9":0.5267,"10":0.5308,"11":0.5282,"12":0.5209,"13":0.5221,"14":0.5401,"15":0.537,"16":0.5417,"17":0.5464,"18":0.5555,"19":0.5525,"20":0.5499,"21":0.5503,"22":0.5515,"23":0.5505,"24":0.5541,"25":0.5523},"40":{"0":0.5296,"1":0.5231,"2":0.5198,"3":0.5219,"4":0.5335,"5":0.5321,"6":0.5265,"7":0.5224,"8":0.5229,"9":0.5262,"10":0.5267,"11":0.5278,"12":0.5229,"13":0.5189,"14":0.5365,"15":0.5389,"16":0.5423,"17":0.5489,"18":0.5546,"19":0.5511,"20":0.5496,"21":0.5494,"22":0.5502,"23":0.5513,"24":0.5547,"25":0.5528},"50":{"0":0.5296,"1":0.5288,"2":0.5221,"3":0.5273,"4":0.5225,"5":0.5311,"6":0.5241,"7":0.5179,"8":0.5197,"9":0.5224,"10":0.5268,"11":0.5254,"12":0.5207,"13":0.5202,"14":0.5357,"15":0.5377,"16":0.54,"17":0.5437,"18":0.555,"19":0.5513,"20":0.5504,"21":0.5504,"22":0.5514,"23":0.5508,"24":0.555,"25":0.5529},"60":{"0":0.5252,"1":0.5113,"2":0.5084,"3":0.4978,"4":0.5192,"5":0.5269,"6":0.5192,"7":0.5162,"8":0.5108,"9":0.5197,"10":0.5276,"11":0.5218,"12":0.5193,"13":0.5138,"14":0.5373,"15":0.5377,"16":0.5424,"17":0.5459,"18":0.5538,"19":0.5533,"20":0.5502,"21":0.5503,"22":0.5512,"23":0.5503,"24":0.555,"25":0.5524},"70":{"0":0.523,"1":0.5132,"2":0.5027,"3":0.4824,"4":0.5119,"5":0.5174,"6":0.5135,"7":0.5085,"8":0.5105,"9":0.5169,"10":0.5239,"11":0.518,"12":0.5089,"13":0.5135,"14":0.5344,"15":0.5344,"16":0.536,"17":0.5444,"18":0.555,"19":0.5528,"20":0.5507,"21":0.5514,"22":0.5504,"23":0.5503,"24":0.5539,"25":0.5532},"80":{"0":0.5127,"1":0.4921,"2":0.4838,"3":0.4903,"4":0.5066,"5":0.5171,"6":0.5111,"7":0.499,"8":0.5028,"9":0.5162,"10":0.5184,"11":0.5179,"12":0.5103,"13":0.5099,"14":0.5322,"15":0.5353,"16":0.5397,"17":0.5469,"18":0.5533,"19":0.5523,"20":0.5486,"21":0.5498,"22":0.5495,"23":0.5496,"24":0.5547,"25":0.5527},"90":{"0":0.5083,"1":0.5024,"2":0.4878,"3":0.4928,"4":0.4954,"5":0.5082,"6":0.5019,"7":0.4948,"8":0.4981,"9":0.5078,"10":0.5135,"11":0.5122,"12":0.5066,"13":0.5043,"14":0.5261,"15":0.5306,"16":0.5335,"17":0.54,"18":0.5542,"19":0.5518,"20":0.5482,"21":0.5503,"22":0.5523,"23":0.5502,"24":0.5539,"25":0.5518},"100":{"0":0.5042,"1":0.4222,"2":0.4591,"3":0.447,"4":0.4909,"5":0.5049,"6":0.4999,"7":0.4878,"8":0.4888,"9":0.5018,"10":0.5149,"11":0.5098,"12":0.5011,"13":0.5,"14":0.526,"15":0.5312,"16":0.5368,"17":0.5426,"18":0.5501,"19":0.5527,"20":0.5498,"21":0.5503,"22":0.5516,"23":0.5492,"24":0.5543,"25":0.5524}}},"bbq_ambig":{"layer":{"20":{"0":0.527388,"1":0.487041,"2":0.47651,"3":0.496273,"4":0.55464,"5":0.542467,"6":0.462422,"7":0.421562,"8":0.364494,"9":0.348184,"10":0.363332,"11":0.383266,"12":0.382822,"13":0.384805,"14":0.395917,"15":0.448164,"16":0.483895,"17":0.433564,"18":0.609348,"19":0.599911,"20":0.613417,"21":0.618067,"22":0.598338,"23":0.600868,"24":0.589722,"25":0.585755},"30":{"0":0.520892,"1":0.482904,"2":0.452917,"3":0.4907,"4":0.549238,"5":0.530773,"6":0.455208,"7":0.413458,"8":0.355878,"9":0.346064,"10":0.364255,"11":0.384258,"12":0.38436,"13":0.38648,"14":0.395404,"15":0.449155,"16":0.483211,"17":0.43394,"18":0.610101,"19":0.599569,"20":0.613315,"21":0.618067,"22":0.597483,"23":0.601176,"24":0.589893,"25":0.585687},"40":{"0":0.507625,"1":0.48116,"2":0.455105,"3":0.477501,"4":0.523867,"5":0.50359,"6":0.440812,"7":0.394105,"8":0.344765,"9":0.340183,"10":0.364665,"11":0.387882,"12":0.388737,"13":0.388737,"14":0.397764,"15":0.448335,"16":0.480613,"17":0.431204,"18":0.610101,"19":0.599638,"20":0.613041,"21":0.617144,"22":0.597825,"23":0.601074,"24":0.590132,"25":0.58596},"50":{"0":0.50253,"1":0.424092,"2":0.43965,"3":0.45066,"4":0.516105,"5":0.506531,"6":0.429802,"7":0.382411,"8":0.348902,"9":0.337277,"10":0.36405,"11":0.39007,"12":0.39007,"13":0.390173,"14":0.401901,"15":0.450797,"16":0.480168,"17":0.433358,"18":0.610237,"19":0.599125,"20":0.612494}}},"bbq_disambig":{"layer":{"20":{"0":0.744888,"1":0.735519,"2":0.723757,"3":0.73275,"4":0.734733,"5":0.738152,"6":0.736169,"7":0.729057,"8":0.70543,"9":0.723757,"10":0.727997,"11":0.714115,"12":0.689428,"13":0.689838,"14":0.737092,"15":0.75053,"16":0.768344,"17":0.767284,"18":0.746393,"19":0.752821,"20":0.752923,"21":0.753847,"22":0.752958,"23":0.753642,"24":0.752547,"25":0.752034},"30":{"0":0.740341,"1":0.731827,"2":0.719141,"3":0.730049,"4":0.735041,"5":0.732989,"6":0.734083,"7":0.726356,"8":0.701498,"9":0.719141,"10":0.725672,"11":0.713021,"12":0.68741,"13":0.689291,"14":0.73487,"15":0.750085,"16":0.768652,"17":0.765712,"18":0.746051,"19":0.753436,"20":0.753744,"21":0.75371,"22":0.752923,"23":0.753676,"24":0.7533,"25":0.752684},"40":{"0":0.734767,"1":0.729707,"2":0.729536,"3":0.706934,"4":0.7175,"5":0.728954,"6":0.725022,"7":0.717431,"8":0.688162,"9":0.707174,"10":0.72068,"11":0.705977,"12":0.685359,"13":0.683683,"14":0.731519,"15":0.747829,"16":0.767729,"17":0.767113,"18":0.746393}}}};
127
+
128
+ var BASELINES = {
129
+ mmlu: 0.5206,
130
+ mmlu_const: 0.5519,
131
+ bbq_ambig: 0.6017,
132
+ bbq_disambig: 0.7542
133
+ };
134
+
135
+ var TASK_LABELS = {
136
+ mmlu: 'MMLU',
137
+ mmlu_const: 'MMLU (Constrained)',
138
+ bbq_ambig: 'BBQ (Ambiguous)',
139
+ bbq_disambig: 'BBQ (Disambiguated)'
140
+ };
141
+
142
+ var currentTask = 'mmlu';
143
+ var currentMode = 'layer';
144
+ var disabledCoeffs = {};
145
+
146
+ /* Mako palette (seaborn): dark purple β†’ indigo β†’ teal β†’ mint */
147
+ var COEFF_COLORS = {
148
+ 10: '#1c101d', 15: '#2c1d37', 20: '#382a54', 25: '#3f3872',
149
+ 30: '#3f488e', 40: '#395d9b', 45: '#3570a0', 50: '#3483a4',
150
+ 60: '#3496a9', 70: '#38a9ac', 75: '#44bcad', 80: '#5fceac',
151
+ 90: '#90dbb3', 100: '#bbe7c8'
152
+ };
153
+ function coeffColor(c) { return COEFF_COLORS[c] || '#888'; }
154
+
155
+ /* ── Build DOM ── */
156
+ var tabs = document.createElement('div');
157
+ tabs.className = 'ls-tabs';
158
+ root.appendChild(tabs);
159
+
160
+ var modeRow = document.createElement('div');
161
+ modeRow.className = 'ls-mode-row';
162
+ root.appendChild(modeRow);
163
+
164
+ var chartWrap = document.createElement('div');
165
+ chartWrap.className = 'ls-chart-wrap';
166
+ root.appendChild(chartWrap);
167
+
168
+ var legend = document.createElement('div');
169
+ legend.className = 'ls-legend';
170
+ root.appendChild(legend);
171
+
172
+ var tooltip = document.createElement('div');
173
+ tooltip.className = 'ls-tooltip';
174
+ root.appendChild(tooltip);
175
+
176
+ /* ── Tabs ── */
177
+ function renderTabs() {
178
+ tabs.innerHTML = '';
179
+ Object.keys(TASK_LABELS).forEach(function(task) {
180
+ var btn = document.createElement('button');
181
+ btn.className = 'ls-tab' + (task === currentTask ? ' active' : '');
182
+ btn.textContent = TASK_LABELS[task];
183
+ btn.addEventListener('click', function() {
184
+ currentTask = task;
185
+ disabledCoeffs = {};
186
+ pickDefaultMode();
187
+ renderAll();
188
+ });
189
+ tabs.appendChild(btn);
190
+ });
191
+ }
192
+
193
+ function pickDefaultMode() {
194
+ currentMode = 'layer';
195
+ }
196
+
197
+ function renderModeButtons() {
198
+ modeRow.innerHTML = '';
199
+ }
200
+
201
+ function getActiveData() {
202
+ var td = DATA[currentTask];
203
+ if (!td) return null;
204
+ return td.layer || null;
205
+ }
206
+
207
+ /* ── Chart rendering with D3 ── */
208
+ function renderChart() {
209
+ chartWrap.innerHTML = '';
210
+ var activeData = getActiveData();
211
+ if (!activeData) {
212
+ chartWrap.innerHTML = '<div style="padding:24px;color:var(--muted-color);text-align:center;">No data</div>';
213
+ return;
214
+ }
215
+
216
+ var coeffs = Object.keys(activeData).map(Number).sort(function(a,b){return a-b;});
217
+ var layers = [];
218
+ coeffs.forEach(function(c) {
219
+ Object.keys(activeData[String(c)]).forEach(function(l) {
220
+ var ln = Number(l);
221
+ if (layers.indexOf(ln) === -1) layers.push(ln);
222
+ });
223
+ });
224
+ layers.sort(function(a,b){return a-b;});
225
+
226
+ var baseline = BASELINES[currentTask];
227
+
228
+ /* SVG dimensions */
229
+ var W = 720, H = 360;
230
+ var margin = {top: 16, right: 20, bottom: 44, left: 54};
231
+ var iw = W - margin.left - margin.right;
232
+ var ih = H - margin.top - margin.bottom;
233
+
234
+ var svg = d3.select(chartWrap).append('svg')
235
+ .attr('viewBox', '0 0 ' + W + ' ' + H)
236
+ .attr('width', '100%')
237
+ .style('display', 'block');
238
+
239
+ var g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
240
+
241
+ /* Scales */
242
+ var xScale = d3.scaleLinear().domain([d3.min(layers), d3.max(layers)]).range([0, iw]);
243
+
244
+ var allVals = [];
245
+ coeffs.forEach(function(c) {
246
+ if (disabledCoeffs[c]) return;
247
+ var cd = activeData[String(c)];
248
+ layers.forEach(function(l) {
249
+ var v = cd[String(l)];
250
+ if (v != null) allVals.push(v);
251
+ });
252
+ });
253
+ if (baseline != null) allVals.push(baseline);
254
+ var yMin = d3.min(allVals) - 0.01;
255
+ var yMax = d3.max(allVals) + 0.01;
256
+ var yScale = d3.scaleLinear().domain([yMin, yMax]).range([ih, 0]).nice();
257
+
258
+ /* Grid lines */
259
+ var yTicks = yScale.ticks(6);
260
+ yTicks.forEach(function(t) {
261
+ g.append('line')
262
+ .attr('x1', 0).attr('x2', iw)
263
+ .attr('y1', yScale(t)).attr('y2', yScale(t))
264
+ .attr('stroke', 'var(--border-color, rgba(0,0,0,0.08))')
265
+ .attr('stroke-dasharray', '2,3');
266
+ });
267
+
268
+ /* Baseline (only if defined for this task) */
269
+ if (baseline != null) {
270
+ g.append('line')
271
+ .attr('x1', 0).attr('x2', iw)
272
+ .attr('y1', yScale(baseline)).attr('y2', yScale(baseline))
273
+ .attr('stroke', 'var(--muted-color, #aaa)')
274
+ .attr('stroke-width', 1.5)
275
+ .attr('stroke-dasharray', '6,4');
276
+
277
+ g.append('text')
278
+ .attr('x', iw + 4).attr('y', yScale(baseline))
279
+ .attr('dy', '0.35em')
280
+ .attr('font-size', '10px')
281
+ .attr('fill', 'var(--muted-color, #aaa)')
282
+ .text('Base');
283
+ }
284
+
285
+ /* Line generator */
286
+ var line = d3.line()
287
+ .x(function(d){ return xScale(d.layer); })
288
+ .y(function(d){ return yScale(d.acc); })
289
+ .defined(function(d){ return d.acc != null; })
290
+ .curve(d3.curveMonotoneX);
291
+
292
+ /* Draw lines */
293
+ coeffs.forEach(function(c) {
294
+ if (disabledCoeffs[c]) return;
295
+ var cd = activeData[String(c)];
296
+ var pts = layers.map(function(l) {
297
+ return { layer: l, acc: cd[String(l)] || null };
298
+ }).filter(function(d){ return d.acc != null; });
299
+
300
+ var color = coeffColor(c);
301
+
302
+ g.append('path')
303
+ .datum(pts)
304
+ .attr('fill', 'none')
305
+ .attr('stroke', color)
306
+ .attr('stroke-width', 2)
307
+ .attr('stroke-linecap', 'round')
308
+ .attr('d', line)
309
+ .attr('opacity', 0.85);
310
+
311
+ /* Dots */
312
+ g.selectAll('.dot-c' + c)
313
+ .data(pts).enter()
314
+ .append('circle')
315
+ .attr('cx', function(d){ return xScale(d.layer); })
316
+ .attr('cy', function(d){ return yScale(d.acc); })
317
+ .attr('r', 2.5)
318
+ .attr('fill', color)
319
+ .attr('stroke', color)
320
+ .attr('stroke-width', 0.5);
321
+ });
322
+
323
+ /* Axes */
324
+ var xAxis = d3.axisBottom(xScale)
325
+ .ticks(13)
326
+ .tickFormat(function(d){ return d; });
327
+ g.append('g')
328
+ .attr('transform', 'translate(0,' + ih + ')')
329
+ .call(xAxis)
330
+ .selectAll('text')
331
+ .attr('fill', 'var(--tick-color, var(--text-color, #333))')
332
+ .attr('font-size', '11px');
333
+
334
+ g.append('text')
335
+ .attr('x', iw / 2).attr('y', ih + 36)
336
+ .attr('text-anchor', 'middle')
337
+ .attr('fill', 'var(--text-color, #333)')
338
+ .attr('font-size', '12px')
339
+ .attr('font-weight', 600)
340
+ .text('Layer');
341
+
342
+ var yAxis = d3.axisLeft(yScale)
343
+ .ticks(6)
344
+ .tickFormat(function(d){ return (d * 100).toFixed(1); });
345
+ g.append('g')
346
+ .call(yAxis)
347
+ .selectAll('text')
348
+ .attr('fill', 'var(--tick-color, var(--text-color, #333))')
349
+ .attr('font-size', '11px');
350
+
351
+ g.append('text')
352
+ .attr('transform', 'translate(-40,' + (ih / 2) + ') rotate(-90)')
353
+ .attr('text-anchor', 'middle')
354
+ .attr('fill', 'var(--text-color, #333)')
355
+ .attr('font-size', '12px')
356
+ .attr('font-weight', 600)
357
+ .text('Accuracy (%)');
358
+
359
+ /* Style axis lines */
360
+ svg.selectAll('.domain').attr('stroke', 'var(--border-color, rgba(0,0,0,0.15))');
361
+ svg.selectAll('.tick line').attr('stroke', 'var(--border-color, rgba(0,0,0,0.08))');
362
+
363
+ /* Hover interaction */
364
+ var hoverLine = g.append('line')
365
+ .attr('y1', 0).attr('y2', ih)
366
+ .attr('stroke', 'var(--text-color, #333)')
367
+ .attr('stroke-width', 0.8)
368
+ .attr('stroke-dasharray', '3,3')
369
+ .attr('opacity', 0);
370
+
371
+ var hoverRect = g.append('rect')
372
+ .attr('width', iw).attr('height', ih)
373
+ .attr('fill', 'none')
374
+ .attr('pointer-events', 'all');
375
+
376
+ hoverRect.on('mousemove', function(event) {
377
+ var coords = d3.pointer(event);
378
+ var mx = coords[0];
379
+ var layerVal = Math.round(xScale.invert(mx));
380
+ layerVal = Math.max(d3.min(layers), Math.min(d3.max(layers), layerVal));
381
+
382
+ hoverLine.attr('x1', xScale(layerVal)).attr('x2', xScale(layerVal)).attr('opacity', 1);
383
+
384
+ var html = '<div style="font-weight:600;margin-bottom:4px;">Layer ' + layerVal + '</div>';
385
+ var activeCoeffs = coeffs.filter(function(c){ return !disabledCoeffs[c]; });
386
+ activeCoeffs.forEach(function(c) {
387
+ var cd = activeData[String(c)];
388
+ var val = cd[String(layerVal)];
389
+ if (val != null) {
390
+ var deltaHtml = '';
391
+ if (baseline != null) {
392
+ var delta = val - baseline;
393
+ var sign = delta >= 0 ? '+' : '';
394
+ var dColor = delta >= 0 ? '#10b981' : '#ef4444';
395
+ deltaHtml = '<span style="color:' + dColor + ';font-size:11px;">' + sign + (delta * 100).toFixed(2) + '</span>';
396
+ }
397
+ html += '<div style="display:flex;align-items:center;gap:6px;">' +
398
+ '<span style="width:8px;height:8px;border-radius:50%;background:' + coeffColor(c) + ';flex-shrink:0;"></span>' +
399
+ '<span style="min-width:24px;">c=' + c + '</span>' +
400
+ '<span style="font-weight:600;">' + (val * 100).toFixed(2) + '%</span>' +
401
+ deltaHtml +
402
+ '</div>';
403
+ }
404
+ });
405
+
406
+ tooltip.innerHTML = html;
407
+ tooltip.style.opacity = '1';
408
+
409
+ var wrapRect = chartWrap.getBoundingClientRect();
410
+ var rootRect = root.getBoundingClientRect();
411
+ var tx = coords[0] + margin.left + 16;
412
+ var ty = coords[1] + margin.top - 10;
413
+ var tipW = tooltip.offsetWidth || 180;
414
+ if (tx + tipW > wrapRect.width) tx = coords[0] + margin.left - tipW - 12;
415
+ tooltip.style.left = tx + 'px';
416
+ tooltip.style.top = ty + 'px';
417
+ });
418
+
419
+ hoverRect.on('mouseleave', function() {
420
+ hoverLine.attr('opacity', 0);
421
+ tooltip.style.opacity = '0';
422
+ });
423
+ }
424
+
425
+ /* ── Legend ── */
426
+ function renderLegend() {
427
+ legend.innerHTML = '';
428
+ var activeData = getActiveData();
429
+ if (!activeData) return;
430
+ var coeffs = Object.keys(activeData).map(Number).sort(function(a,b){return a-b;});
431
+
432
+ coeffs.forEach(function(c) {
433
+ var item = document.createElement('span');
434
+ item.className = 'ls-legend-item' + (disabledCoeffs[c] ? ' dimmed' : '');
435
+ item.innerHTML = '<span class="ls-legend-swatch" style="background:' + coeffColor(c) + ';"></span>' +
436
+ '<span>' + c + '</span>';
437
+ item.addEventListener('click', function() {
438
+ disabledCoeffs[c] = !disabledCoeffs[c];
439
+ renderAll();
440
+ });
441
+ legend.appendChild(item);
442
+ });
443
+
444
+ /* Baseline legend entry (only if baseline exists) */
445
+ if (BASELINES[currentTask] != null) {
446
+ var bl = document.createElement('span');
447
+ bl.className = 'ls-legend-baseline';
448
+ bl.innerHTML = '<span class="ls-legend-dash"></span><span>Baseline (' + (BASELINES[currentTask] * 100).toFixed(1) + '%)</span>';
449
+ legend.appendChild(bl);
450
+ }
451
+ }
452
+
453
+ /* ── Render all ── */
454
+ function renderAll() {
455
+ renderTabs();
456
+ renderModeButtons();
457
+ renderChart();
458
+ renderLegend();
459
+ }
460
+
461
+ function waitForD3() {
462
+ if (window.d3 && typeof d3.select === 'function') {
463
+ renderAll();
464
+ } else {
465
+ setTimeout(waitForD3, 100);
466
+ }
467
+ }
468
+ waitForD3();
469
+ })();
470
+ </script>
app/src/content/embeds/norm-growth.html ADDED
@@ -0,0 +1,568 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!--
2
+ Residual Stream Norm Growth
3
+
4
+ Interactive D3.js line chart showing how residual stream norms
5
+ grow across transformer layers in Gemma 2 2B. Marks intervention
6
+ layers with colored dots and shaded coefficient-range zones.
7
+ -->
8
+ <div class="norm-growth"></div>
9
+ <style>
10
+ .norm-growth {
11
+ position: relative;
12
+ font-family: system-ui, -apple-system, sans-serif;
13
+ width: 100%;
14
+ }
15
+
16
+ .norm-growth .chart-card {
17
+ background: var(--page-bg);
18
+ border: 1px solid var(--border-color);
19
+ border-radius: 16px;
20
+ padding: 24px;
21
+ }
22
+
23
+ .norm-growth svg {
24
+ display: block;
25
+ width: 100%;
26
+ }
27
+
28
+ /* Axes */
29
+ .norm-growth .axis path { stroke: none; }
30
+ .norm-growth .axis line { stroke: var(--axis-color, #d1d5db); shape-rendering: crispEdges; }
31
+ .norm-growth .axis text { fill: var(--tick-color, #6b7280); font-size: 11px; }
32
+ .norm-growth .grid line { stroke: var(--grid-color, #e5e7eb); stroke-dasharray: 2, 2; }
33
+ .norm-growth .grid path { stroke: none; }
34
+ .norm-growth .axis-label {
35
+ fill: var(--text-color, #1f2937);
36
+ font-size: 12px;
37
+ font-weight: 600;
38
+ }
39
+
40
+ /* Zone labels */
41
+ .norm-growth .zone-label {
42
+ font-size: 10px;
43
+ font-weight: 600;
44
+ pointer-events: none;
45
+ text-anchor: middle;
46
+ dominant-baseline: hanging;
47
+ opacity: 0.85;
48
+ }
49
+
50
+ .norm-growth .zone-name {
51
+ font-size: 9px;
52
+ font-weight: 500;
53
+ pointer-events: none;
54
+ text-anchor: middle;
55
+ opacity: 0.6;
56
+ }
57
+
58
+ /* Dot labels on hover */
59
+ .norm-growth .dot-label {
60
+ font-size: 10px;
61
+ font-weight: 600;
62
+ text-anchor: middle;
63
+ pointer-events: none;
64
+ }
65
+
66
+ /* Legend */
67
+ .norm-growth .legend-bar {
68
+ display: flex;
69
+ justify-content: center;
70
+ flex-wrap: wrap;
71
+ gap: 6px 16px;
72
+ margin-top: 16px;
73
+ }
74
+
75
+ .norm-growth .legend-item {
76
+ display: inline-flex;
77
+ align-items: center;
78
+ gap: 6px;
79
+ font-size: 11px;
80
+ color: var(--text-color, #1f2937);
81
+ white-space: nowrap;
82
+ }
83
+
84
+ .norm-growth .legend-dot {
85
+ width: 8px;
86
+ height: 8px;
87
+ border-radius: 50%;
88
+ flex-shrink: 0;
89
+ }
90
+
91
+ /* Crosshair */
92
+ .norm-growth .crosshair {
93
+ stroke: var(--muted-color, #9ca3af);
94
+ stroke-width: 1;
95
+ stroke-dasharray: 3, 3;
96
+ opacity: 0.5;
97
+ }
98
+
99
+ .norm-growth .hover-dot {
100
+ stroke: var(--page-bg, #fff);
101
+ stroke-width: 2;
102
+ }
103
+
104
+ /* Dark mode overrides via CSS vars */
105
+ [data-theme="dark"] .norm-growth .grid line { stroke: var(--grid-color, #374151); }
106
+ [data-theme="dark"] .norm-growth .axis line { stroke: var(--axis-color, #4b5563); }
107
+ @media (prefers-color-scheme: dark) {
108
+ :root:not([data-theme="light"]) .norm-growth .grid line { stroke: var(--grid-color, #374151); }
109
+ :root:not([data-theme="light"]) .norm-growth .axis line { stroke: var(--axis-color, #4b5563); }
110
+ }
111
+
112
+ @media (max-width: 600px) {
113
+ .norm-growth .chart-card {
114
+ padding: 14px;
115
+ }
116
+ }
117
+ </style>
118
+ <script>
119
+ (function() {
120
+ var ensureD3 = function(cb) {
121
+ if (window.d3 && typeof window.d3.select === 'function') return cb();
122
+ var s = document.getElementById('d3-cdn-script');
123
+ if (!s) {
124
+ s = document.createElement('script');
125
+ s.id = 'd3-cdn-script';
126
+ s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
127
+ document.head.appendChild(s);
128
+ }
129
+ var onReady = function() {
130
+ if (window.d3 && typeof window.d3.select === 'function') cb();
131
+ };
132
+ s.addEventListener('load', onReady, { once: true });
133
+ if (window.d3) onReady();
134
+ };
135
+
136
+ var bootstrap = function() {
137
+ var candidates = Array.from(document.querySelectorAll('.norm-growth'))
138
+ .filter(function(el) { return !el.dataset.mounted; });
139
+ var container = candidates[candidates.length - 1];
140
+ if (!container) return;
141
+ container.dataset.mounted = 'true';
142
+ container.style.position = 'relative';
143
+
144
+ var d3 = window.d3;
145
+
146
+ /* ── Data ── */
147
+ /* Values read from paper Figure norm.png (b) Residual Stream L2 Norm */
148
+ var data = [
149
+ {layer: 0, norm: 42},
150
+ {layer: 1, norm: 44},
151
+ {layer: 2, norm: 47},
152
+ {layer: 3, norm: 50},
153
+ {layer: 4, norm: 54},
154
+ {layer: 5, norm: 58},
155
+ {layer: 6, norm: 63},
156
+ {layer: 7, norm: 68},
157
+ {layer: 8, norm: 75},
158
+ {layer: 9, norm: 82},
159
+ {layer: 10, norm: 92},
160
+ {layer: 11, norm: 105},
161
+ {layer: 12, norm: 118},
162
+ {layer: 13, norm: 132},
163
+ {layer: 14, norm: 148},
164
+ {layer: 15, norm: 165},
165
+ {layer: 16, norm: 180},
166
+ {layer: 17, norm: 195},
167
+ {layer: 18, norm: 212},
168
+ {layer: 19, norm: 228},
169
+ {layer: 20, norm: 248},
170
+ {layer: 21, norm: 275},
171
+ {layer: 22, norm: 310},
172
+ {layer: 23, norm: 370},
173
+ {layer: 24, norm: 500},
174
+ {layer: 25, norm: 1100}
175
+ ];
176
+
177
+ /* Intervention layers */
178
+ var interventions = [
179
+ { layer: 5, label: 'L5 (BBQ)', color: '#EC4899', benchmark: 'BBQ' },
180
+ { layer: 10, label: 'L10', color: '#06B6D4', benchmark: '' },
181
+ { layer: 12, label: 'L12 (XSTest)', color: '#F97316', benchmark: 'XSTest' },
182
+ { layer: 20, label: 'L20', color: '#3B82F6', benchmark: '' },
183
+ { layer: 21, label: 'L21 (HarmBench)', color: '#7C3AED', benchmark: 'HarmBench' },
184
+ { layer: 24, label: 'L24 (MMLU/GSM8K)',color: '#10B981', benchmark: 'MMLU/GSM8K' }
185
+ ];
186
+
187
+ /* Coefficient range zones */
188
+ var zones = [
189
+ { x0: 5, x1: 10, fill: '#10B981', label: 'c = 10-20', name: 'Low layers' },
190
+ { x0: 15, x1: 20, fill: '#F59E0B', label: 'c = 30-60', name: 'Mid layers' },
191
+ { x0: 22, x1: 25, fill: '#EF4444', label: 'c = 60-100', name: 'Deep layers' }
192
+ ];
193
+
194
+ var LINE_COLOR = '#4F46E5';
195
+ var margin = { top: 20, right: 28, bottom: 54, left: 56 };
196
+ var hasAnimated = false;
197
+
198
+ /* ── Tooltip ── */
199
+ var tip = document.createElement('div');
200
+ Object.assign(tip.style, {
201
+ position: 'absolute', top: '0', left: '0',
202
+ transform: 'translate(-9999px, -9999px)',
203
+ pointerEvents: 'none', padding: '10px 14px',
204
+ borderRadius: '10px', fontSize: '12px', lineHeight: '1.5',
205
+ border: '1px solid var(--border-color, #e5e5e5)',
206
+ background: 'var(--surface-bg, #f9fafb)', color: 'var(--text-color, #1f2937)',
207
+ boxShadow: '0 6px 28px rgba(0,0,0,.18)',
208
+ backdropFilter: 'saturate(1.12) blur(10px)',
209
+ WebkitBackdropFilter: 'saturate(1.12) blur(10px)',
210
+ opacity: '0', transition: 'opacity .12s ease',
211
+ zIndex: '1000', maxWidth: '260px'
212
+ });
213
+ var tipInner = document.createElement('div');
214
+ tip.appendChild(tipInner);
215
+ container.appendChild(tip);
216
+
217
+ function showTip(event, html) {
218
+ tipInner.innerHTML = html;
219
+ var coords = d3.pointer(event, container);
220
+ var tipRect = tip.getBoundingClientRect();
221
+ var cx = container.getBoundingClientRect().width;
222
+ var cy = container.getBoundingClientRect().height;
223
+ var tx = coords[0] + 16;
224
+ var ty = coords[1] - 12;
225
+ if (tx + (tipRect.width || 200) > cx - 10) tx = coords[0] - (tipRect.width || 200) - 16;
226
+ if (ty < 10) ty = coords[1] + 20;
227
+ if (ty + (tipRect.height || 80) > cy - 10) ty = coords[1] - (tipRect.height || 80) - 10;
228
+ tip.style.transform = 'translate(' + Math.round(tx) + 'px,' + Math.round(ty) + 'px)';
229
+ tip.style.opacity = '1';
230
+ }
231
+ function hideTip() {
232
+ tip.style.opacity = '0';
233
+ tip.style.transform = 'translate(-9999px, -9999px)';
234
+ }
235
+
236
+ /* ── DOM structure ── */
237
+ var card = document.createElement('div');
238
+ card.className = 'chart-card';
239
+ container.appendChild(card);
240
+
241
+ var svgWrap = document.createElement('div');
242
+ svgWrap.style.position = 'relative';
243
+ card.appendChild(svgWrap);
244
+
245
+ var svg = d3.select(svgWrap).append('svg');
246
+ var defs = svg.append('defs');
247
+
248
+ /* Gradient for area fill */
249
+ var gradId = 'norm-area-grad-' + Math.random().toString(36).slice(2);
250
+ var grad = defs.append('linearGradient')
251
+ .attr('id', gradId)
252
+ .attr('x1', '0').attr('y1', '0')
253
+ .attr('x2', '0').attr('y2', '1');
254
+ grad.append('stop').attr('offset', '0%').attr('stop-color', LINE_COLOR).attr('stop-opacity', 0.25);
255
+ grad.append('stop').attr('offset', '100%').attr('stop-color', LINE_COLOR).attr('stop-opacity', 0.02);
256
+
257
+ var gRoot = svg.append('g');
258
+ var gGrid = gRoot.append('g').attr('class', 'grid');
259
+ var gZones = gRoot.append('g').attr('class', 'zones');
260
+ var gArea = gRoot.append('g').attr('class', 'area-layer');
261
+ var gLine = gRoot.append('g').attr('class', 'line-layer');
262
+ var gDots = gRoot.append('g').attr('class', 'dots-layer');
263
+ var gLabels = gRoot.append('g').attr('class', 'labels-layer').style('pointer-events', 'none');
264
+ var gHover = gRoot.append('g').attr('class', 'hover-layer').style('pointer-events', 'none');
265
+ var gAxes = gRoot.append('g').attr('class', 'axes');
266
+
267
+ /* Legend */
268
+ var legendBar = document.createElement('div');
269
+ legendBar.className = 'legend-bar';
270
+ interventions.forEach(function(iv) {
271
+ var item = document.createElement('span');
272
+ item.className = 'legend-item';
273
+ var dot = document.createElement('span');
274
+ dot.className = 'legend-dot';
275
+ dot.style.background = iv.color;
276
+ var txt = document.createElement('span');
277
+ txt.textContent = iv.label;
278
+ item.appendChild(dot);
279
+ item.appendChild(txt);
280
+ legendBar.appendChild(item);
281
+ });
282
+ card.appendChild(legendBar);
283
+
284
+ /* ── Render ── */
285
+ function render(animate) {
286
+ var width = container.clientWidth || 700;
287
+ var height = Math.max(300, Math.round(width / 2.2));
288
+ svg.attr('width', width).attr('height', height)
289
+ .attr('viewBox', '0 0 ' + width + ' ' + height)
290
+ .attr('preserveAspectRatio', 'xMidYMid meet');
291
+ gRoot.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
292
+
293
+ var innerW = width - margin.left - margin.right;
294
+ var innerH = height - margin.top - margin.bottom;
295
+
296
+ var xScale = d3.scaleLinear().domain([0, 25]).range([0, innerW]);
297
+ var yScale = d3.scaleLinear().domain([0, 1200]).range([innerH, 0]).nice();
298
+
299
+ /* Clear */
300
+ gGrid.selectAll('*').remove();
301
+ gZones.selectAll('*').remove();
302
+ gArea.selectAll('*').remove();
303
+ gLine.selectAll('*').remove();
304
+ gDots.selectAll('*').remove();
305
+ gLabels.selectAll('*').remove();
306
+ gHover.selectAll('*').remove();
307
+ gAxes.selectAll('*').remove();
308
+
309
+ /* Grid */
310
+ gGrid.append('g').attr('class', 'grid')
311
+ .call(d3.axisLeft(yScale).tickSize(-innerW).tickFormat(''))
312
+ .call(function(g) { g.select('.domain').remove(); });
313
+
314
+ /* Shaded coefficient zones */
315
+ zones.forEach(function(z) {
316
+ var zx = xScale(z.x0);
317
+ var zw = xScale(z.x1) - xScale(z.x0);
318
+
319
+ gZones.append('rect')
320
+ .attr('x', zx).attr('y', 0)
321
+ .attr('width', zw).attr('height', innerH)
322
+ .attr('fill', z.fill)
323
+ .attr('opacity', 0.07)
324
+ .attr('rx', 4).attr('ry', 4);
325
+
326
+ /* Zone label at top */
327
+ gZones.append('text')
328
+ .attr('class', 'zone-label')
329
+ .attr('x', zx + zw / 2)
330
+ .attr('y', 6)
331
+ .attr('fill', z.fill)
332
+ .text(z.label);
333
+
334
+ gZones.append('text')
335
+ .attr('class', 'zone-name')
336
+ .attr('x', zx + zw / 2)
337
+ .attr('y', 34)
338
+ .attr('fill', z.fill)
339
+ .text(z.name);
340
+ });
341
+
342
+ /* Area fill */
343
+ var areaGen = d3.area()
344
+ .x(function(d) { return xScale(d.layer); })
345
+ .y0(innerH)
346
+ .y1(function(d) { return yScale(d.norm); })
347
+ .curve(d3.curveMonotoneX);
348
+
349
+ gArea.append('path')
350
+ .attr('d', areaGen(data))
351
+ .attr('fill', 'url(#' + gradId + ')');
352
+
353
+ /* Line */
354
+ var lineGen = d3.line()
355
+ .x(function(d) { return xScale(d.layer); })
356
+ .y(function(d) { return yScale(d.norm); })
357
+ .curve(d3.curveMonotoneX);
358
+
359
+ var linePath = gLine.append('path')
360
+ .attr('d', lineGen(data))
361
+ .attr('fill', 'none')
362
+ .attr('stroke', LINE_COLOR)
363
+ .attr('stroke-width', 2.5)
364
+ .attr('stroke-linejoin', 'round')
365
+ .attr('stroke-linecap', 'round');
366
+
367
+ /* Animate line draw-in */
368
+ if (animate) {
369
+ var totalLen = linePath.node().getTotalLength();
370
+ linePath
371
+ .attr('stroke-dasharray', totalLen)
372
+ .attr('stroke-dashoffset', totalLen)
373
+ .transition().duration(1400).ease(d3.easeCubicOut)
374
+ .attr('stroke-dashoffset', 0);
375
+
376
+ /* Fade-in area */
377
+ gArea.select('path')
378
+ .attr('opacity', 0)
379
+ .transition().delay(400).duration(800).ease(d3.easeCubicOut)
380
+ .attr('opacity', 1);
381
+ }
382
+
383
+ /* Intervention dots */
384
+ var ivMap = {};
385
+ interventions.forEach(function(iv) { ivMap[iv.layer] = iv; });
386
+
387
+ interventions.forEach(function(iv) {
388
+ var pt = data.find(function(d) { return d.layer === iv.layer; });
389
+ if (!pt) return;
390
+ var cx = xScale(pt.layer);
391
+ var cy = yScale(pt.norm);
392
+
393
+ /* Outer glow ring */
394
+ gDots.append('circle')
395
+ .attr('cx', cx).attr('cy', cy)
396
+ .attr('r', 8)
397
+ .attr('fill', iv.color)
398
+ .attr('opacity', 0.15);
399
+
400
+ /* Main dot */
401
+ var dot = gDots.append('circle')
402
+ .attr('cx', cx).attr('cy', cy)
403
+ .attr('r', 5)
404
+ .attr('fill', iv.color)
405
+ .attr('stroke', 'var(--page-bg, #fff)')
406
+ .attr('stroke-width', 2)
407
+ .style('cursor', 'pointer');
408
+
409
+ if (animate) {
410
+ dot.attr('r', 0)
411
+ .transition().delay(600 + iv.layer * 40).duration(400).ease(d3.easeBackOut)
412
+ .attr('r', 5);
413
+ }
414
+
415
+ /* Persistent layer label for key benchmarks */
416
+ if (iv.benchmark) {
417
+ var labelY = cy - 14;
418
+ /* If label would overlap the top, push below */
419
+ if (labelY < 24) labelY = cy + 18;
420
+
421
+ gLabels.append('text')
422
+ .attr('class', 'dot-label')
423
+ .attr('x', cx).attr('y', labelY)
424
+ .attr('fill', iv.color)
425
+ .text(iv.label)
426
+ .attr('opacity', animate ? 0 : 0.85)
427
+ .transition().delay(animate ? 900 + iv.layer * 40 : 0).duration(400)
428
+ .attr('opacity', 0.85);
429
+ }
430
+ });
431
+
432
+ /* Axes */
433
+ gAxes.append('g').attr('class', 'axis x-axis')
434
+ .attr('transform', 'translate(0,' + innerH + ')')
435
+ .call(d3.axisBottom(xScale).ticks(Math.min(13, Math.floor(innerW / 50))).tickFormat(function(d) { return 'L' + d; }));
436
+
437
+ gAxes.append('g').attr('class', 'axis y-axis')
438
+ .call(d3.axisLeft(yScale).ticks(6));
439
+
440
+ /* Axis labels */
441
+ gAxes.append('text').attr('class', 'axis-label')
442
+ .attr('text-anchor', 'middle')
443
+ .attr('x', innerW / 2).attr('y', innerH + 42)
444
+ .text('Transformer Layer');
445
+
446
+ gAxes.append('text').attr('class', 'axis-label')
447
+ .attr('text-anchor', 'middle')
448
+ .attr('transform', 'translate(-42,' + (innerH / 2) + ') rotate(-90)')
449
+ .text('Residual Stream Norm');
450
+
451
+ /* ── Hover / crosshair ── */
452
+ var crosshairV = gHover.append('line').attr('class', 'crosshair').style('display', 'none');
453
+ var crosshairH = gHover.append('line').attr('class', 'crosshair').style('display', 'none');
454
+ var hoverDot = gHover.append('circle').attr('class', 'hover-dot')
455
+ .attr('r', 5).attr('fill', LINE_COLOR).style('display', 'none');
456
+
457
+ var hoverRect = svg.selectAll('.hover-rect').data([0]).join('rect')
458
+ .attr('class', 'hover-rect')
459
+ .attr('x', margin.left).attr('y', margin.top)
460
+ .attr('width', innerW).attr('height', innerH)
461
+ .attr('fill', 'transparent')
462
+ .style('cursor', 'crosshair');
463
+
464
+ var bisect = d3.bisector(function(d) { return d.layer; }).left;
465
+
466
+ hoverRect.on('mousemove', function(event) {
467
+ var coords = d3.pointer(event, gRoot.node());
468
+ var mx = coords[0];
469
+ var layerVal = xScale.invert(mx);
470
+ layerVal = Math.max(0, Math.min(25, layerVal));
471
+
472
+ /* Find closest data point */
473
+ var idx = bisect(data, layerVal);
474
+ if (idx >= data.length) idx = data.length - 1;
475
+ if (idx > 0) {
476
+ var d0 = data[idx - 1];
477
+ var d1 = data[idx];
478
+ if (Math.abs(layerVal - d0.layer) < Math.abs(layerVal - d1.layer)) idx = idx - 1;
479
+ }
480
+ var pt = data[idx];
481
+
482
+ var cx = xScale(pt.layer);
483
+ var cy = yScale(pt.norm);
484
+
485
+ crosshairV.style('display', null)
486
+ .attr('x1', cx).attr('x2', cx)
487
+ .attr('y1', 0).attr('y2', innerH);
488
+
489
+ crosshairH.style('display', null)
490
+ .attr('x1', 0).attr('x2', innerW)
491
+ .attr('y1', cy).attr('y2', cy);
492
+
493
+ hoverDot.style('display', null)
494
+ .attr('cx', cx).attr('cy', cy);
495
+
496
+ /* Build tooltip */
497
+ var html = '<div style="font-weight:700;margin-bottom:4px">Layer ' + pt.layer + '</div>';
498
+ html += '<div style="font-size:16px;font-weight:700;color:' + LINE_COLOR + '">Norm: ' + pt.norm + '</div>';
499
+
500
+ /* Check if this is an intervention layer */
501
+ var iv = ivMap[pt.layer];
502
+ if (iv) {
503
+ html += '<div style="margin-top:6px;display:flex;align-items:center;gap:6px">';
504
+ html += '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + iv.color + '"></span>';
505
+ html += '<span style="font-weight:600;color:' + iv.color + '">' + iv.label + '</span>';
506
+ html += '</div>';
507
+ }
508
+
509
+ /* Check if in a zone */
510
+ zones.forEach(function(z) {
511
+ if (pt.layer >= z.x0 && pt.layer <= z.x1) {
512
+ html += '<div style="margin-top:4px;font-size:11px;color:var(--muted-color, #6b7280)">Coefficient range: <strong>' + z.label + '</strong></div>';
513
+ }
514
+ });
515
+
516
+ /* Growth rate from previous layer */
517
+ if (idx > 0) {
518
+ var prevNorm = data[idx - 1].norm;
519
+ var growth = ((pt.norm - prevNorm) / prevNorm * 100).toFixed(1);
520
+ html += '<div style="margin-top:3px;font-size:11px;color:var(--muted-color, #6b7280)">Growth from L' + data[idx - 1].layer + ': +' + growth + '%</div>';
521
+ }
522
+
523
+ showTip(event, html);
524
+ });
525
+
526
+ hoverRect.on('mouseleave', function() {
527
+ crosshairV.style('display', 'none');
528
+ crosshairH.style('display', 'none');
529
+ hoverDot.style('display', 'none');
530
+ hideTip();
531
+ });
532
+ }
533
+
534
+ /* ── Init ── */
535
+ render(false);
536
+
537
+ /* IntersectionObserver for initial scroll-in animation */
538
+ if (window.IntersectionObserver) {
539
+ var obs = new IntersectionObserver(function(entries) {
540
+ entries.forEach(function(entry) {
541
+ if (entry.isIntersecting && !hasAnimated) {
542
+ hasAnimated = true;
543
+ render(true);
544
+ obs.disconnect();
545
+ }
546
+ });
547
+ }, { threshold: 0.15 });
548
+ obs.observe(container);
549
+ } else {
550
+ hasAnimated = true;
551
+ render(true);
552
+ }
553
+
554
+ /* Resize */
555
+ if (window.ResizeObserver) {
556
+ new ResizeObserver(function() { if (hasAnimated) render(false); }).observe(container);
557
+ } else {
558
+ window.addEventListener('resize', function() { render(false); });
559
+ }
560
+ };
561
+
562
+ if (document.readyState === 'loading') {
563
+ document.addEventListener('DOMContentLoaded', function() { ensureD3(bootstrap); }, { once: true });
564
+ } else {
565
+ ensureD3(bootstrap);
566
+ }
567
+ })();
568
+ </script>