diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..753302dfa5dc44e58b7893b71d0903151d489686 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,18 @@ +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.mov filter=lfs diff=lfs merge=lfs -text +*.avi filter=lfs diff=lfs merge=lfs -text +*.wav filter=lfs diff=lfs merge=lfs -text +*.csv filter=lfs diff=lfs merge=lfs -text +*.json filter=lfs diff=lfs merge=lfs -text +*.pdf filter=lfs diff=lfs merge=lfs -text +# the package and package lock should not be tracked +package.json -filter -diff -merge text +package-lock.json -filter -diff -merge text +# Notion imported images should NOT be in LFS (needed for Docker build) +app/src/content/assets/image/image_27877f1c*.png -filter -diff -merge text +app/scripts/notion-importer/output/** -filter -diff -merge text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3344bbd884b24bdc19198c0b2725a89b7593f84e --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Python +__pycache__ +*.py[cod] +*.so +.Python +env/ +venv/ +*.egg-info/ +dist/ +build/ +*.egg +.idea/ +.vscode/ +.astro/ +.claude/ +*.swp +.DS_Store +# Node +node_modules/ +*.log +*.env +*.cache +.notion-to-md + +app/scripts/latex-to-mdx/output/ +app/scripts/notion-importer/output/**/* +app/src/content/embeds/typography/generated + +# PDF export +app/public/*.pdf +app/public/*.png +app/public/*.jpg +app/public/data/**/* + +.astro/ + +# Template sync temporary directories +.template-sync/ +.temp-*/ +.backup-*/ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..37c494c4121d071d3dcc01d7dadcf608332cf983 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Stage 1: Build Astro static site +FROM node:18-slim AS build + +WORKDIR /app + +COPY app/package*.json ./ + +RUN npm install + +COPY app/ . + +# Ensure public/data is a real directory (not symlink) +RUN set -e; \ + rm -rf public/data; \ + mkdir -p public/data; \ + cp -a src/content/assets/data/. public/data/ + +RUN npm run build + +# Stage 2: Serve with nginx +FROM nginx:alpine + +COPY --from=build /app/dist /app/dist + +COPY nginx.conf /etc/nginx/nginx.conf + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +RUN mkdir -p /var/cache/nginx /var/run /var/log/nginx /var/lib/nginx/body /tmp && \ + chmod -R 777 /var/cache/nginx /var/run /var/log/nginx /var/lib/nginx /tmp /app + +EXPOSE 7860 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..b267a53137822114e4c0bcef2e6383aaf52a70f1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,33 @@ +Creative Commons Attribution 4.0 International License + +Copyright (c) 2024 Thibaud Frere + +This work is licensed under the Creative Commons Attribution 4.0 International License. +To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/ +or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + +You are free to: + + Share — copy and redistribute the material in any medium or format + Adapt — remix, transform, and build upon the material for any purpose, even commercially. + +The licensor cannot revoke these freedoms as long as you follow the license terms. + +Under the following terms: + + 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. + + No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. + +Notices: + + 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. + + 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. + +--- + +For the source code and technical implementation: +- The source code is available at: https://huggingface.co/spaces/tfrere/research-article-template +- Third-party figures and assets are excluded from this license and marked in their captions +- Dependencies and third-party libraries maintain their respective licenses diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ed71581773aee678ff459fe2577bf7e981cfe47a --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +--- +title: 'Control Reinforcement Learning' +short_desc: 'Interpretable Token-Level Steering of LLMs via SAE Features' +emoji: 🎛️ +colorFrom: indigo +colorTo: purple +sdk: docker +pinned: false +header: mini +app_port: 7860 +tags: + - research-article-template + - research paper + - scientific paper + - data visualization +thumbnail: https://huggingface.co/spaces/tfrere/research-paper-template/thumb.jpg +--- +
+ +# Research Article Template + +[![License: CC BY 4.0](https://img.shields.io/badge/License-CC%20BY%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by/4.0/) +[![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg)](https://nodejs.org/) +[![Astro](https://img.shields.io/badge/Astro-4.10.0-orange.svg)](https://astro.build/) +[![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/tfrere/research-article-template) + + +**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. + +**[Try the live demo & documentation →](https://huggingface.co/spaces/tfrere/research-article-template)** + +
+ +## 🚀 Quick Start + +### Option 1: Duplicate on Hugging Face (Recommended) + +1. Visit **[🤗 Research Article Template](https://huggingface.co/spaces/tfrere/research-article-template)** +2. Click **"Duplicate this Space"** +3. Clone your new repository: + ```bash + git clone git@hf.co:spaces// + cd + ``` + +### Option 2: Clone Directly + +```bash +git clone https://github.com/tfrere/research-article-template.git +cd research-article-template +``` + +### Installation + +```bash +# Install Node.js 20+ (use nvm for version management) +nvm install 20 +nvm use 20 + +# Install Git LFS and pull assets +git lfs install +git lfs pull + +# Install dependencies +cd app +npm install + +# Start development server +npm run dev +``` + +Visit `http://localhost:4321` to see your site! + +## 🎯 Who This Is For + +- **Scientists** writing modern, web-native research papers +- **Educators** creating interactive, explorable lessons +- **Researchers** who want to focus on ideas, not infrastructure +- **Anyone** who values clear, engaging technical communication + +## 🌟 Inspired by Distill + +This template carries forward the spirit of [Distill](https://distill.pub/) (2016–2021), pushing interactive scientific writing even further with: +- Accessible, high-quality explanations +- Reproducible, production-ready demos +- Modern web technologies and best practices + +## 🤝 Contributing + +We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. + +### Ways to Contribute + +- **Report bugs** - Open an issue with detailed information +- **Suggest features** - Share ideas for improvements +- **Improve documentation** - Help others get started +- **Submit code** - Fix bugs or add features +- **Join discussions** - Share feedback and ideas + +## 📄 License + +This project is licensed under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). + +- **Diagrams and text**: CC-BY 4.0 +- **Source code**: Available on [Hugging Face](https://huggingface.co/spaces/tfrere/research-article-template) +- **Third-party figures**: Excluded and marked in captions + +## 🙏 Acknowledgments + +- Inspired by [Distill](https://distill.pub/) and the interactive scientific writing movement +- Built with [Astro](https://astro.build/), [MDX](https://mdxjs.com/), and modern web technologies +- Community feedback and contributions from researchers worldwide + +## 📞 Support + +- **[Community Discussions](https://huggingface.co/spaces/tfrere/research-article-template/discussions)** - Ask questions and share ideas +- **[Report Issues](https://huggingface.co/spaces/tfrere/research-article-template/discussions?status=open&type=issue)** - Bug reports and feature requests +- **Contact**: [@tfrere](https://huggingface.co/tfrere) on Hugging Face + +--- + +**Made with ❤️ for the scientific community** \ No newline at end of file diff --git a/app/astro.config.mjs b/app/astro.config.mjs new file mode 100644 index 0000000000000000000000000000000000000000..71baa66de17c73c4ec23b82dbaf5273c11732287 --- /dev/null +++ b/app/astro.config.mjs @@ -0,0 +1,78 @@ +import { defineConfig } from 'astro/config'; +import mdx from '@astrojs/mdx'; +import svelte from '@astrojs/svelte'; +import compressor from 'astro-compressor'; +import remarkMath from 'remark-math'; +import rehypeKatex from 'rehype-katex'; +import remarkFootnotes from 'remark-footnotes'; +import rehypeSlug from 'rehype-slug'; +import rehypeAutolinkHeadings from 'rehype-autolink-headings'; +import rehypeCodeCopy from './plugins/rehype/code-copy.mjs'; +import rehypeCitation from 'rehype-citation'; +import rehypeReferencesAndFootnotes from './plugins/rehype/post-citation.mjs'; +import remarkIgnoreCitationsInCode from './plugins/remark/ignore-citations-in-code.mjs'; +import remarkUnwrapCitationLinks from './plugins/remark/unwrap-citation-links.mjs'; +import remarkDirective from 'remark-directive'; +import remarkOutputContainer from './plugins/remark/output-container.mjs'; +import rehypeRestoreAtInCode from './plugins/rehype/restore-at-in-code.mjs'; +import rehypeWrapTables from './plugins/rehype/wrap-tables.mjs'; +import rehypeWrapOutput from './plugins/rehype/wrap-outputs.mjs'; +// Built-in Shiki (dual themes) — no rehype-pretty-code + +// Plugins moved to app/plugins/* + +export default defineConfig({ + output: 'static', + integrations: [ + mdx(), + svelte(), + // Precompress output with Gzip only (Brotli disabled due to server module mismatch) + compressor({ brotli: false, gzip: true }) + ], + devToolbar: { + enabled: false + }, + markdown: { + shikiConfig: { + themes: { + light: 'github-light', + dark: 'github-dark' + }, + defaultColor: false, + wrap: false, + langAlias: { + // Map MDX fences to TSX for better JSX tokenization + mdx: 'tsx' + } + }, + remarkPlugins: [ + remarkUnwrapCitationLinks, + remarkIgnoreCitationsInCode, + remarkMath, + [remarkFootnotes, { inlineNotes: true }], + remarkDirective, + remarkOutputContainer + ], + rehypePlugins: [ + rehypeSlug, + [rehypeAutolinkHeadings, { behavior: 'wrap' }], + [rehypeKatex, { + trust: true, + }], + [rehypeCitation, { + bibliography: 'src/content/bibliography.bib', + linkCitations: true, + csl: "apa", + noCite: false, + suppressBibliography: false, + }], + rehypeReferencesAndFootnotes, + rehypeRestoreAtInCode, + rehypeCodeCopy, + rehypeWrapOutput, + rehypeWrapTables + ] + } +}); + + diff --git a/app/package-lock.json b/app/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..753ed0c3dfa27a035ecac302c5e337f5b42a14b6 Binary files /dev/null and b/app/package-lock.json differ diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000000000000000000000000000000000000..3520b0dfbec04671e01d1815f1a6e820d6d310aa Binary files /dev/null and b/app/package.json differ diff --git a/app/plugins/rehype/code-copy.mjs b/app/plugins/rehype/code-copy.mjs new file mode 100644 index 0000000000000000000000000000000000000000..29b135ee039c2af2f468bc836874f55a0a78ca17 --- /dev/null +++ b/app/plugins/rehype/code-copy.mjs @@ -0,0 +1,94 @@ +// Minimal rehype plugin to wrap code blocks with a copy button +// Exported as a standalone module to keep astro.config.mjs lean +export default function rehypeCodeCopy() { + return (tree) => { + // Walk the tree; lightweight visitor to find

+    const visit = (node, parent) => {
+      if (!node || typeof node !== 'object') return;
+      const children = Array.isArray(node.children) ? node.children : [];
+      if (node.tagName === 'pre' && children.some(c => c.tagName === 'code')) {
+        // Find code child
+        const code = children.find(c => c.tagName === 'code');
+        // Determine if single-line block: prefer Shiki lines, then text content
+        const countLinesFromShiki = () => {
+          const isLineEl = (el) => el && el.type === 'element' && el.tagName === 'span' && Array.isArray(el.properties?.className) && el.properties.className.includes('line');
+          const hasNonWhitespaceText = (node) => {
+            if (!node) return false;
+            if (node.type === 'text') return /\S/.test(String(node.value || ''));
+            const kids = Array.isArray(node.children) ? node.children : [];
+            return kids.some(hasNonWhitespaceText);
+          };
+          const collectLines = (node, acc) => {
+            if (!node || typeof node !== 'object') return;
+            if (isLineEl(node)) acc.push(node);
+            const kids = Array.isArray(node.children) ? node.children : [];
+            kids.forEach((k) => collectLines(k, acc));
+          };
+          const lines = [];
+          collectLines(code, lines);
+          const nonEmpty = lines.filter((ln) => hasNonWhitespaceText(ln)).length;
+          return nonEmpty || 0;
+        };
+        const countLinesFromText = () => {
+          // Parse raw text content of the  node including nested spans
+          const extractText = (node) => {
+            if (!node) return '';
+            if (node.type === 'text') return String(node.value || '');
+            const kids = Array.isArray(node.children) ? node.children : [];
+            return kids.map(extractText).join('');
+          };
+          const raw = extractText(code);
+          if (!raw || !/\S/.test(raw)) return 0;
+          return raw.split('\n').filter(line => /\S/.test(line)).length;
+        };
+        const lines = countLinesFromShiki() || countLinesFromText();
+        const isSingleLine = lines <= 1;
+        // Also treat code blocks shorter than a threshold as single-line (defensive)
+        if (!isSingleLine) {
+          const approxChars = (() => {
+            const extract = (n) => Array.isArray(n?.children) ? n.children.map(extract).join('') : (n?.type === 'text' ? String(n.value||'') : '');
+            return extract(code).length;
+          })();
+          if (approxChars < 6) {
+            node.__forceSingle = true;
+          }
+        }
+        // Replace 
 with wrapper div.code-card containing button + pre
+        const wrapper = {
+          type: 'element',
+          tagName: 'div',
+          properties: { className: ['code-card'].concat((isSingleLine || node.__forceSingle) ? ['no-copy'] : []) },
+          children: (isSingleLine || node.__forceSingle) ? [ node ] : [
+            {
+              type: 'element',
+              tagName: 'button',
+              properties: { className: ['code-copy', 'button--ghost'], type: 'button', 'aria-label': 'Copy code' },
+              children: [
+                {
+                  type: 'element',
+                  tagName: 'svg',
+                  properties: { viewBox: '0 0 24 24', 'aria-hidden': 'true', focusable: 'false' },
+                  children: [
+                    { 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: [] }
+                  ]
+                }
+              ]
+            },
+            node
+          ]
+        };
+        if (parent && Array.isArray(parent.children)) {
+          const idx = parent.children.indexOf(node);
+          if (idx !== -1) parent.children[idx] = wrapper;
+        }
+        return; // don't visit nested
+      }
+      children.forEach((c) => visit(c, node));
+    };
+    visit(tree, null);
+  };
+}
+
+
+
+
diff --git a/app/plugins/rehype/post-citation.mjs b/app/plugins/rehype/post-citation.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..b8ce7678d2d1cb09007b552838f8103c74b997b0
--- /dev/null
+++ b/app/plugins/rehype/post-citation.mjs
@@ -0,0 +1,449 @@
+// rehype plugin to post-process citations and footnotes at build-time
+// - Normalizes the bibliography into 
    with
  1. +// - Linkifies DOI/URL occurrences inside references +// - Appends back-reference links (↩ back: 1, 2, ...) from each reference to in-text citation anchors +// - Cleans up footnotes block (.footnotes) + +export default function rehypeReferencesAndFootnotes() { + return (tree) => { + const isElement = (n) => n && typeof n === 'object' && n.type === 'element'; + const getChildren = (n) => (Array.isArray(n?.children) ? n.children : []); + + const walk = (node, parent, fn) => { + if (!node || typeof node !== 'object') return; + fn && fn(node, parent); + const kids = getChildren(node); + for (const child of kids) walk(child, node, fn); + }; + + const ensureArray = (v) => (Array.isArray(v) ? v : v != null ? [v] : []); + + const hasClass = (el, name) => { + const cn = ensureArray(el?.properties?.className).map(String); + return cn.includes(name); + }; + + const setAttr = (el, key, val) => { + el.properties = el.properties || {}; + if (val == null) delete el.properties[key]; + else el.properties[key] = val; + }; + + const getAttr = (el, key) => (el?.properties ? el.properties[key] : undefined); + + // Shared helpers for backlinks + backrefs block + const collectBacklinksForIdSet = (idSet, anchorPrefix) => { + const idToBacklinks = new Map(); + const idToAnchorNodes = new Map(); + if (!idSet || idSet.size === 0) return { idToBacklinks, idToAnchorNodes }; + walk(tree, null, (node) => { + if (!isElement(node) || node.tagName !== 'a') return; + const href = String(getAttr(node, 'href') || ''); + if (!href.startsWith('#')) return; + const id = href.slice(1); + if (!idSet.has(id)) return; + // Ensure a stable id + let anchorId = String(getAttr(node, 'id') || ''); + if (!anchorId) { + const list = idToBacklinks.get(id) || []; + anchorId = `${anchorPrefix}-${id}-${list.length + 1}`; + setAttr(node, 'id', anchorId); + } + const list = idToBacklinks.get(id) || []; + list.push(anchorId); + idToBacklinks.set(id, list); + const nodes = idToAnchorNodes.get(id) || []; + nodes.push(node); + idToAnchorNodes.set(id, nodes); + }); + return { idToBacklinks, idToAnchorNodes }; + }; + + const createBackIcon = () => ({ + type: 'element', + tagName: 'svg', + properties: { + className: ['back-icon'], + width: 12, + height: 12, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': 2, + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + 'aria-hidden': 'true', + focusable: 'false' + }, + children: [ + { type: 'element', tagName: 'line', properties: { x1: 12, y1: 19, x2: 12, y2: 5 }, children: [] }, + { type: 'element', tagName: 'polyline', properties: { points: '5 12 12 5 19 12' }, children: [] } + ] + }); + + const appendBackrefsBlock = (listElement, idToBacklinks, ariaLabel) => { + if (!listElement || !idToBacklinks || idToBacklinks.size === 0) return; + for (const li of getChildren(listElement)) { + if (!isElement(li) || li.tagName !== 'li') continue; + const id = String(getAttr(li, 'id') || ''); + if (!id) continue; + const keys = idToBacklinks.get(id); + if (!keys || !keys.length) continue; + // Remove pre-existing .backrefs in this li to avoid duplicates + li.children = getChildren(li).filter((n) => !(isElement(n) && n.tagName === 'small' && hasClass(n, 'backrefs'))); + const small = { + type: 'element', + tagName: 'small', + properties: { className: ['backrefs'] }, + children: [] + }; + if (keys.length === 1) { + // Single backlink: just the icon wrapped in the anchor + const a = { + type: 'element', + tagName: 'a', + properties: { href: `#${keys[0]}`, 'aria-label': ariaLabel }, + children: [createBackIcon()] + }; + small.children.push(a); + } else { + // Multiple backlinks: icon + label + numbered links + small.children.push(createBackIcon()); + small.children.push({ type: 'text', value: ' back: ' }); + keys.forEach((backId, idx) => { + small.children.push({ + type: 'element', + tagName: 'a', + properties: { href: `#${backId}`, 'aria-label': ariaLabel }, + children: [{ type: 'text', value: String(idx + 1) }] + }); + if (idx < keys.length - 1) small.children.push({ type: 'text', value: ', ' }); + }); + } + li.children.push(small); + } + }; + // Remove default back-reference anchors generated by remark-footnotes inside a footnote item + const getTextContent = (el) => { + if (!el) return ''; + const stack = [el]; + let out = ''; + while (stack.length) { + const cur = stack.pop(); + if (!cur) continue; + if (cur.type === 'text') out += String(cur.value || ''); + const kids = getChildren(cur); + for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]); + } + return out; + }; + + const removeFootnoteBackrefAnchors = (el) => { + if (!isElement(el)) return; + const kids = getChildren(el); + for (let i = kids.length - 1; i >= 0; i--) { + const child = kids[i]; + if (isElement(child)) { + if ( + child.tagName === 'a' && ( + getAttr(child, 'data-footnote-backref') != null || + hasClass(child, 'footnote-backref') || + String(getAttr(child, 'role') || '').toLowerCase() === 'doc-backlink' || + String(getAttr(child, 'aria-label') || '').toLowerCase().includes('back to content') || + String(getAttr(child, 'href') || '').startsWith('#fnref') || + // Fallback: text-based detection like "↩" or "↩2" + /^\s*↩\s*\d*\s*$/u.test(getTextContent(child)) + ) + ) { + // Remove the anchor + el.children.splice(i, 1); + continue; + } + // Recurse into element + removeFootnoteBackrefAnchors(child); + // If a wrapper like or became empty, remove it + const becameKids = getChildren(child); + if ((child.tagName === 'sup' || child.tagName === 'span') && (!becameKids || becameKids.length === 0)) { + el.children.splice(i, 1); + } + } + } + }; + + + const normDoiHref = (href) => { + if (!href) return href; + const DUP = /https?:\/\/(?:dx\.)?doi\.org\/(?:https?:\/\/(?:dx\.)?doi\.org\/)+/gi; + const ONE = /https?:\/\/(?:dx\.)?doi\.org\/(10\.[^\s<>"']+)/i; + href = String(href).replace(DUP, 'https://doi.org/'); + const m = href.match(ONE); + return m ? `https://doi.org/${m[1]}` : href; + }; + + const DOI_BARE = /\b10\.[0-9]{4,9}\/[\-._;()\/:A-Z0-9]+\b/gi; + const URL_GEN = /\bhttps?:\/\/[^\s<>()"']+/gi; + + const linkifyTextNode = (textNode) => { + const text = String(textNode.value || ''); + let last = 0; + const parts = []; + const pushText = (s) => { if (s) parts.push({ type: 'text', value: s }); }; + + const matches = []; + // Collect URL matches + let m; + URL_GEN.lastIndex = 0; + while ((m = URL_GEN.exec(text)) !== null) { + matches.push({ type: 'url', start: m.index, end: URL_GEN.lastIndex, raw: m[0] }); + } + // Collect DOI matches + DOI_BARE.lastIndex = 0; + while ((m = DOI_BARE.exec(text)) !== null) { + matches.push({ type: 'doi', start: m.index, end: DOI_BARE.lastIndex, raw: m[0] }); + } + matches.sort((a, b) => a.start - b.start); + + for (const match of matches) { + if (match.start < last) continue; // overlapping + pushText(text.slice(last, match.start)); + if (match.type === 'url') { + const href = normDoiHref(match.raw); + const doiOne = href.match(/https?:\/\/(?:dx\.)?doi\.org\/(10\.[^\s<>"']+)/i); + const a = { + type: 'element', + tagName: 'a', + properties: { href, target: '_blank', rel: 'noopener noreferrer' }, + children: [{ type: 'text', value: doiOne ? doiOne[1] : href }] + }; + parts.push(a); + } else { + const href = `https://doi.org/${match.raw}`; + const a = { + type: 'element', + tagName: 'a', + properties: { href, target: '_blank', rel: 'noopener noreferrer' }, + children: [{ type: 'text', value: match.raw }] + }; + parts.push(a); + } + last = match.end; + } + + pushText(text.slice(last)); + return parts; + }; + + const linkifyInElement = (el) => { + const kids = getChildren(el); + for (let i = 0; i < kids.length; i++) { + const child = kids[i]; + if (!child) continue; + if (child.type === 'text') { + const replacement = linkifyTextNode(child); + if (replacement.length === 1 && replacement[0].type === 'text') continue; + // Replace the single text node with multiple nodes + el.children.splice(i, 1, ...replacement); + i += replacement.length - 1; + } else if (isElement(child)) { + if (child.tagName === 'a') { + const href = normDoiHref(getAttr(child, 'href')); + setAttr(child, 'href', href); + const m = String(href || '').match(/https?:\/\/(?:dx\.)?doi\.org\/(10\.[^\s<>"']+)/i); + if (m && (!child.children || child.children.length === 0)) { + child.children = [{ type: 'text', value: m[1] }]; + } + continue; + } + linkifyInElement(child); + } + } + // Deduplicate adjacent identical anchors + for (let i = 1; i < el.children.length; i++) { + const prev = el.children[i - 1]; + const curr = el.children[i]; + if (isElement(prev) && isElement(curr) && prev.tagName === 'a' && curr.tagName === 'a') { + const key = `${getAttr(prev, 'href') || ''}|${(prev.children?.[0]?.value) || ''}`; + const key2 = `${getAttr(curr, 'href') || ''}|${(curr.children?.[0]?.value) || ''}`; + if (key === key2) { + el.children.splice(i, 1); + i--; + } + } + } + }; + + // Find references container and normalize its list + const findReferencesRoot = () => { + let found = null; + walk(tree, null, (node) => { + if (found) return; + if (!isElement(node)) return; + + // Ignore headers (h1, h2, h3, h4, h5, h6) - we only want container elements + if (/^h[1-6]$/i.test(node.tagName)) return; + + const id = getAttr(node, 'id'); + if (id === 'references' || id === 'refs' || hasClass(node, 'references') || hasClass(node, 'bibliography')) { + found = node; + } + }); + return found; + }; + + const toOrderedList = (container) => { + // If there is already an
      , use it; otherwise convert common structures + let ol = getChildren(container).find((c) => isElement(c) && c.tagName === 'ol'); + if (!ol) { + ol = { type: 'element', tagName: 'ol', properties: { className: ['references'] }, children: [] }; + const candidates = getChildren(container).filter((n) => isElement(n)); + if (candidates.length) { + for (const node of candidates) { + if (hasClass(node, 'csl-entry') || node.tagName === 'li' || node.tagName === 'p' || node.tagName === 'div') { + const li = { type: 'element', tagName: 'li', properties: {}, children: getChildren(node) }; + if (getAttr(node, 'id')) setAttr(li, 'id', getAttr(node, 'id')); + ol.children.push(li); + } + } + } + // Replace container children by the new ol + container.children = [ol]; + } + if (!hasClass(ol, 'references')) { + const cls = ensureArray(ol.properties?.className).map(String); + if (!cls.includes('references')) cls.push('references'); + ol.properties = ol.properties || {}; + ol.properties.className = cls; + } + return ol; + }; + + const refsRoot = findReferencesRoot(); + let refsOl = null; + const refIdSet = new Set(); + const refIdToExternalHref = new Map(); + + if (refsRoot) { + // Add a unique id to avoid collisions with user-created headers + setAttr(refsRoot, 'id', 'bibliography-references-list'); + setAttr(refsRoot, 'data-bibliography-block', 'true'); + + refsOl = toOrderedList(refsRoot); + // Collect item ids and linkify their content + for (const li of getChildren(refsOl)) { + if (!isElement(li) || li.tagName !== 'li') continue; + if (!getAttr(li, 'id')) { + // Try to find a nested element with id to promote + const nestedWithId = getChildren(li).find((n) => isElement(n) && getAttr(n, 'id')); + if (nestedWithId) setAttr(li, 'id', getAttr(nestedWithId, 'id')); + } + const id = getAttr(li, 'id'); + if (id) refIdSet.add(String(id)); + linkifyInElement(li); + // Record first external link href (e.g., DOI/URL) if present + if (id) { + let externalHref = null; + const stack = [li]; + while (stack.length) { + const cur = stack.pop(); + const kids = getChildren(cur); + for (const k of kids) { + if (isElement(k) && k.tagName === 'a') { + const href = String(getAttr(k, 'href') || ''); + if (/^https?:\/\//i.test(href)) { + externalHref = href; + break; + } + } + if (isElement(k)) stack.push(k); + } + if (externalHref) break; + } + if (externalHref) refIdToExternalHref.set(String(id), externalHref); + } + } + setAttr(refsRoot, 'data-built-refs', '1'); + } + + // Collect in-text anchors that point to references ids + const { idToBacklinks: refIdToBacklinks, idToAnchorNodes: refIdToCitationAnchors } = collectBacklinksForIdSet(refIdSet, 'refctx'); + + // Append backlinks into references list items + appendBackrefsBlock(refsOl, refIdToBacklinks, 'Back to citation'); + + // Rewrite in-text citation anchors to external link when available + if (refIdToCitationAnchors.size > 0) { + for (const [id, anchors] of refIdToCitationAnchors.entries()) { + const ext = refIdToExternalHref.get(id); + if (!ext) continue; + for (const a of anchors) { + setAttr(a, 'data-ref-id', id); + setAttr(a, 'href', ext); + const existingTarget = getAttr(a, 'target'); + if (!existingTarget) setAttr(a, 'target', '_blank'); + const rel = String(getAttr(a, 'rel') || ''); + const relSet = new Set(rel ? rel.split(/\s+/) : []); + relSet.add('noopener'); + relSet.add('noreferrer'); + setAttr(a, 'rel', Array.from(relSet).join(' ')); + } + } + } + + // Footnotes cleanup + backrefs harmonized with references + const cleanupFootnotes = () => { + let root = null; + walk(tree, null, (node) => { + if (!isElement(node)) return; + if (hasClass(node, 'footnotes')) root = node; + }); + if (!root) return { root: null, ol: null, idSet: new Set() }; + // Remove
      direct children + root.children = getChildren(root).filter((n) => !(isElement(n) && n.tagName === 'hr')); + // Ensure an
        + let ol = getChildren(root).find((c) => isElement(c) && c.tagName === 'ol'); + if (!ol) { + ol = { type: 'element', tagName: 'ol', properties: {}, children: [] }; + const items = getChildren(root).filter((n) => isElement(n) && (n.tagName === 'li' || hasClass(n, 'footnote') || n.tagName === 'p' || n.tagName === 'div')); + if (items.length) { + for (const it of items) { + const li = { type: 'element', tagName: 'li', properties: {}, children: getChildren(it) }; + // Promote nested id if present (e.g.,

        ) + const nestedWithId = getChildren(it).find((n) => isElement(n) && getAttr(n, 'id')); + if (nestedWithId) setAttr(li, 'id', getAttr(nestedWithId, 'id')); + ol.children.push(li); + } + } + root.children = [ol]; + } + // For existing structures, try to promote ids from children when missing + for (const li of getChildren(ol)) { + if (!isElement(li) || li.tagName !== 'li') continue; + if (!getAttr(li, 'id')) { + const nestedWithId = getChildren(li).find((n) => isElement(n) && getAttr(n, 'id')); + if (nestedWithId) setAttr(li, 'id', getAttr(nestedWithId, 'id')); + } + // Remove default footnote backrefs anywhere inside (to avoid duplication) + removeFootnoteBackrefAnchors(li); + } + setAttr(root, 'data-built-footnotes', '1'); + // Collect id set + const idSet = new Set(); + for (const li of getChildren(ol)) { + if (!isElement(li) || li.tagName !== 'li') continue; + const id = getAttr(li, 'id'); + if (id) idSet.add(String(id)); + } + return { root, ol, idSet }; + }; + + const { root: footRoot, ol: footOl, idSet: footIdSet } = cleanupFootnotes(); + + // Collect in-text anchors pointing to footnotes + const { idToBacklinks: footIdToBacklinks } = collectBacklinksForIdSet(footIdSet, 'footctx'); + + // Append backlinks into footnote list items (identical pattern to references) + appendBackrefsBlock(footOl, footIdToBacklinks, 'Back to footnote call'); + }; +} + + diff --git a/app/plugins/rehype/restore-at-in-code.mjs b/app/plugins/rehype/restore-at-in-code.mjs new file mode 100644 index 0000000000000000000000000000000000000000..09db2b1fb8720cefeb7a7d94ea85ba4db47b1612 --- /dev/null +++ b/app/plugins/rehype/restore-at-in-code.mjs @@ -0,0 +1,22 @@ +// Rehype plugin to restore '@' inside code nodes after rehype-citation ran +export default function rehypeRestoreAtInCode() { + return (tree) => { + const restoreInNode = (node) => { + if (!node || typeof node !== 'object') return; + const isText = node.type === 'text'; + if (isText && typeof node.value === 'string' && node.value.includes('__AT_SENTINEL__')) { + node.value = node.value.replace(/__AT_SENTINEL__/g, '@'); + } + const isCodeEl = node.type === 'element' && node.tagName === 'code'; + const children = Array.isArray(node.children) ? node.children : []; + if (isCodeEl && children.length) { + children.forEach(restoreInNode); + return; + } + children.forEach(restoreInNode); + }; + restoreInNode(tree); + }; +} + + diff --git a/app/plugins/rehype/wrap-outputs.mjs b/app/plugins/rehype/wrap-outputs.mjs new file mode 100644 index 0000000000000000000000000000000000000000..307047febe085ffa78f2468978e588bc3749b148 --- /dev/null +++ b/app/plugins/rehype/wrap-outputs.mjs @@ -0,0 +1,38 @@ +// Wrap plain-text content inside

        into a
        +export default function rehypeWrapOutput() {
        +  return (tree) => {
        +    const isWhitespace = (value) => typeof value === 'string' && !/\S/.test(value);
        +    const extractText = (node) => {
        +      if (!node) return '';
        +      if (node.type === 'text') return String(node.value || '');
        +      const kids = Array.isArray(node.children) ? node.children : [];
        +      return kids.map(extractText).join('');
        +    };
        +    const visit = (node) => {
        +      if (!node || typeof node !== 'object') return;
        +      const children = Array.isArray(node.children) ? node.children : [];
        +      if (node.type === 'element' && node.tagName === 'section') {
        +        const className = node.properties?.className || [];
        +        const classes = Array.isArray(className) ? className : [className].filter(Boolean);
        +        if (classes.includes('code-output')) {
        +          const meaningful = children.filter((c) => !(c.type === 'text' && isWhitespace(c.value)));
        +          if (meaningful.length === 1) {
        +            const only = meaningful[0];
        +            const isPlainParagraph = only.type === 'element' && only.tagName === 'p' && (only.children || []).every((c) => c.type === 'text');
        +            const isPlainText = only.type === 'text';
        +            if (isPlainParagraph || isPlainText) {
        +              const text = isPlainText ? String(only.value || '') : extractText(only);
        +              node.children = [
        +                { type: 'element', tagName: 'pre', properties: {}, children: [ { type: 'text', value: text } ] }
        +              ];
        +            }
        +          }
        +        }
        +      }
        +      children.forEach(visit);
        +    };
        +    visit(tree);
        +  };
        +}
        +
        +
        diff --git a/app/plugins/rehype/wrap-tables.mjs b/app/plugins/rehype/wrap-tables.mjs
        new file mode 100644
        index 0000000000000000000000000000000000000000..fc7944cb737ba8cfd2cbed28b66e2527c0234f89
        --- /dev/null
        +++ b/app/plugins/rehype/wrap-tables.mjs
        @@ -0,0 +1,43 @@
        +// rehype plugin: wrap bare  elements in a 
        container +// so that tables stay width:100% while enabling horizontal scroll when content overflows + +export default function rehypeWrapTables() { + return (tree) => { + const isElement = (n) => n && typeof n === 'object' && n.type === 'element'; + const getChildren = (n) => (Array.isArray(n?.children) ? n.children : []); + + const walk = (node, parent, fn) => { + if (!node || typeof node !== 'object') return; + fn && fn(node, parent); + const kids = getChildren(node); + for (const child of kids) walk(child, node, fn); + }; + + const ensureArray = (v) => (Array.isArray(v) ? v : v != null ? [v] : []); + const hasClass = (el, name) => ensureArray(el?.properties?.className).map(String).includes(name); + + const wrapTable = (tableNode, parent) => { + if (!parent || !Array.isArray(parent.children)) return; + // Don't double-wrap if already inside .table-scroll + if (parent.tagName === 'div' && hasClass(parent, 'table-scroll')) return; + + const wrapper = { + type: 'element', + tagName: 'div', + properties: { className: ['table-scroll'] }, + children: [tableNode] + }; + + const idx = parent.children.indexOf(tableNode); + if (idx >= 0) parent.children.splice(idx, 1, wrapper); + }; + + walk(tree, null, (node, parent) => { + if (!isElement(node)) return; + if (node.tagName !== 'table') return; + wrapTable(node, parent); + }); + }; +} + + diff --git a/app/plugins/remark/ignore-citations-in-code.mjs b/app/plugins/remark/ignore-citations-in-code.mjs new file mode 100644 index 0000000000000000000000000000000000000000..b5c3e279088bcbd325bdb2d031de77ed48fa5591 --- /dev/null +++ b/app/plugins/remark/ignore-citations-in-code.mjs @@ -0,0 +1,21 @@ +// Remark plugin to ignore citations inside code (block and inline) +export default function remarkIgnoreCitationsInCode() { + return (tree) => { + const visit = (node) => { + if (!node || typeof node !== 'object') return; + const type = node.type; + if (type === 'code' || type === 'inlineCode') { + if (typeof node.value === 'string' && node.value.includes('@')) { + // Use a sentinel to avoid rehype-citation, will be restored later in rehype + node.value = node.value.replace(/@/g, '__AT_SENTINEL__'); + } + return; // do not traverse into code + } + const children = Array.isArray(node.children) ? node.children : []; + children.forEach(visit); + }; + visit(tree); + }; +} + + diff --git a/app/plugins/remark/output-container.mjs b/app/plugins/remark/output-container.mjs new file mode 100644 index 0000000000000000000000000000000000000000..bb25220416a44e22007345265acb8d2eb803e93b --- /dev/null +++ b/app/plugins/remark/output-container.mjs @@ -0,0 +1,23 @@ +// Transform `:::output ... :::` into a
        wrapper +// Requires remark-directive to be applied before this plugin + +export default function remarkOutputContainer() { + return (tree) => { + const visit = (node) => { + if (!node || typeof node !== 'object') return; + + if (node.type === 'containerDirective' && node.name === 'output') { + node.data = node.data || {}; + node.data.hName = 'section'; + node.data.hProperties = { className: ['code-output'] }; + } + + const children = Array.isArray(node.children) ? node.children : []; + for (const child of children) visit(child); + }; + + visit(tree); + }; +} + + diff --git a/app/plugins/remark/outputs-container.mjs b/app/plugins/remark/outputs-container.mjs new file mode 100644 index 0000000000000000000000000000000000000000..5602aca8e635e00de98f49704be7e51e4f3e87b0 --- /dev/null +++ b/app/plugins/remark/outputs-container.mjs @@ -0,0 +1,23 @@ +// Transform `:::outputs ... :::` into a
        wrapper +// Requires remark-directive to be applied before this plugin + +export default function remarkOutputsContainer() { + return (tree) => { + const visit = (node) => { + if (!node || typeof node !== 'object') return; + + if (node.type === 'containerDirective' && node.name === 'outputs') { + node.data = node.data || {}; + node.data.hName = 'section'; + node.data.hProperties = { className: ['code-outputs'] }; + } + + const children = Array.isArray(node.children) ? node.children : []; + for (const child of children) visit(child); + }; + + visit(tree); + }; +} + + diff --git a/app/plugins/remark/unwrap-citation-links.mjs b/app/plugins/remark/unwrap-citation-links.mjs new file mode 100644 index 0000000000000000000000000000000000000000..89afd8d9b63d311aa6642a231741e8b219a6a962 --- /dev/null +++ b/app/plugins/remark/unwrap-citation-links.mjs @@ -0,0 +1,57 @@ +// Plugin remark pour transformer les liens markdown contenant des citations en citations simples +// Transforme [@reference](url) en [@reference] +export default function remarkUnwrapCitationLinks() { + return (tree) => { + // Fonction helper pour extraire le contenu textuel d'un nœud + const getTextContent = (node) => { + if (!node) return ''; + if (node.type === 'text') return node.value || ''; + if (Array.isArray(node.children)) { + return node.children.map(getTextContent).join(''); + } + return ''; + }; + + const visit = (node, parent) => { + if (!node || typeof node !== 'object') return; + + // Parcourir les enfants d'abord (post-order traversal) + const children = Array.isArray(node.children) ? node.children : []; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + visit(child, node); + } + + // Si c'est un nœud de type 'link', vérifier son contenu + if (node.type === 'link' && parent && Array.isArray(parent.children)) { + // Récupérer le contenu textuel du lien + const textContent = getTextContent(node); + + // Debug + console.log('🔍 Link trouvé:', { + text: textContent, + url: node.url, + matches: /^@\w+/.test(textContent.trim()) + }); + + // Vérifier si c'est une citation (commence par @) + if (textContent && /^@\w+/.test(textContent.trim())) { + // Trouver l'index du nœud dans son parent + const index = parent.children.indexOf(node); + + if (index !== -1) { + console.log('✅ Transformation:', textContent); + // Remplacer le nœud link par un nœud text simple + parent.children[index] = { + type: 'text', + value: textContent.trim() + }; + } + } + } + }; + + visit(tree, null); + }; +} + diff --git a/app/postcss.config.mjs b/app/postcss.config.mjs new file mode 100644 index 0000000000000000000000000000000000000000..65fe6e9fd4437c66b3b2e303bd091a66cff025e5 --- /dev/null +++ b/app/postcss.config.mjs @@ -0,0 +1,14 @@ +// PostCSS config enabling Custom Media Queries +// Allows usage of: @media (--bp-content-collapse) { ... } + +import postcssCustomMedia from 'postcss-custom-media'; +import postcssPresetEnv from 'postcss-preset-env'; + +export default { + plugins: [ + postcssCustomMedia(), + postcssPresetEnv({ + stage: 0 + }) + ] +}; diff --git a/app/public/flowchart-svg.svg b/app/public/flowchart-svg.svg new file mode 100644 index 0000000000000000000000000000000000000000..df08be979961f01ae0a594114e2f7ab651e7741c --- /dev/null +++ b/app/public/flowchart-svg.svg @@ -0,0 +1,474 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/public/hf-space-parent-listener.js b/app/public/hf-space-parent-listener.js new file mode 100644 index 0000000000000000000000000000000000000000..6713d73b4ec6635b0562cd87a17685636433f8e5 --- /dev/null +++ b/app/public/hf-space-parent-listener.js @@ -0,0 +1,70 @@ +/** + * Script pour la fenêtre parente des Spaces Hugging Face + * Ce script écoute les messages de l'iframe et met à jour l'URL de la fenêtre parente + * + * Instructions d'utilisation : + * 1. Ajoutez ce script à votre Space Hugging Face dans le fichier app.py ou dans un composant Gradio + * 2. Ou utilisez-le dans une page HTML qui contient votre iframe + */ + +(function () { + 'use strict'; + + console.log('HF Space Parent Listener initialized'); + + // Écouter les messages de l'iframe + window.addEventListener('message', function (event) { + console.log('Received message from iframe:', event.data); + + // Vérifier le type de message + if (event.data && event.data.type) { + switch (event.data.type) { + case 'urlChange': + case 'anchorChange': + case 'HF_SPACE_URL_UPDATE': + handleUrlChange(event.data); + break; + default: + console.log('Unknown message type:', event.data.type); + } + } + }); + + function handleUrlChange(data) { + try { + const hash = data.hash || data.anchorId; + const url = data.url; + + if (hash) { + // Mettre à jour l'URL avec la nouvelle ancre + const newUrl = new URL(window.location); + newUrl.hash = hash; + + // Utiliser replaceState pour éviter d'ajouter une entrée dans l'historique + window.history.replaceState(null, '', newUrl.toString()); + + console.log('Updated parent URL to:', newUrl.toString()); + + // Optionnel : faire défiler vers l'élément correspondant dans la page parente + const targetElement = document.querySelector(hash); + if (targetElement) { + targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } + } catch (error) { + console.error('Error updating parent URL:', error); + } + } + + // Fonction utilitaire pour tester la communication + window.testIframeCommunication = function () { + console.log('Testing iframe communication...'); + const iframe = document.querySelector('iframe'); + if (iframe) { + iframe.contentWindow.postMessage({ type: 'test' }, '*'); + } else { + console.log('No iframe found'); + } + }; + +})(); diff --git a/app/public/og.png b/app/public/og.png new file mode 100644 index 0000000000000000000000000000000000000000..86d91b5b4f663b2ab6b18746219df4872a56cdc1 --- /dev/null +++ b/app/public/og.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dacac0b7026402fd2c00cbb878474f6399db1d7c5a89e079148ba97558432143 +size 136514 diff --git a/app/public/scripts/color-palettes.js b/app/public/scripts/color-palettes.js new file mode 100644 index 0000000000000000000000000000000000000000..370b1f464142e0d9280855b18f8f636db810ea6e --- /dev/null +++ b/app/public/scripts/color-palettes.js @@ -0,0 +1,274 @@ +// Global color palettes generator and watcher +// - Observes CSS variable --primary-color and theme changes +// - Generates categorical, sequential, and diverging palettes (OKLCH/OKLab) +// - Exposes results as CSS variables on :root +// - Supports variable color counts per palette via CSS vars +// - Dispatches a 'palettes:updated' CustomEvent after each update + +(() => { + const MODE = { cssRoot: document.documentElement }; + + const getCssVar = (name) => { + try { return getComputedStyle(MODE.cssRoot).getPropertyValue(name).trim(); } catch { return ''; } + }; + const getIntFromCssVar = (name, fallback) => { + const raw = getCssVar(name); + if (!raw) return fallback; + const v = parseInt(String(raw), 10); + if (Number.isNaN(v)) return fallback; + return v; + }; + const clamp = (n, min, max) => Math.max(min, Math.min(max, n)); + + // Color math (OKLab/OKLCH) + const srgbToLinear = (u) => (u <= 0.04045 ? u / 12.92 : Math.pow((u + 0.055) / 1.055, 2.4)); + const linearToSrgb = (u) => (u <= 0.0031308 ? 12.92 * u : 1.055 * Math.pow(Math.max(0, u), 1 / 2.4) - 0.055); + const rgbToOklab = (r, g, b) => { + const rl = srgbToLinear(r), gl = srgbToLinear(g), bl = srgbToLinear(b); + const l = Math.cbrt(0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl); + const m = Math.cbrt(0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl); + const s = Math.cbrt(0.0883024619 * rl + 0.2817188376 * gl + 0.6299787005 * bl); + const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s; + const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s; + const b2 = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s; + return { L, a, b: b2 }; + }; + const oklabToRgb = (L, a, b) => { + const l_ = L + 0.3963377774 * a + 0.2158037573 * b; + const m_ = L - 0.1055613458 * a - 0.0638541728 * b; + const s_ = L - 0.0894841775 * a - 1.2914855480 * b; + const l = l_ * l_ * l_; + const m = m_ * m_ * m_; + const s = s_ * s_ * s_; + const r = linearToSrgb(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s); + const g = linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s); + const b3 = linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s); + return { r, g, b: b3 }; + }; + const oklchToOklab = (L, C, hDeg) => { const h = (hDeg * Math.PI) / 180; return { L, a: C * Math.cos(h), b: C * Math.sin(h) }; }; + 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 }; }; + const clamp01 = (x) => Math.min(1, Math.max(0, x)); + const isInGamut = ({ r, g, b }) => r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1; + const toHex = ({ r, g, b }) => { + const R = Math.round(clamp01(r) * 255), G = Math.round(clamp01(g) * 255), B = Math.round(clamp01(b) * 255); + const h = (n) => n.toString(16).padStart(2, '0'); + return `#${h(R)}${h(G)}${h(B)}`.toUpperCase(); + }; + 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)); }; + 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; } }; + + // Get primary color in OKLCH format to preserve precision + const getPrimaryOKLCH = () => { + const css = getCssVar('--primary-color'); + if (!css) return null; + + // For OKLCH colors, return the exact values without conversion + if (css.includes('oklch')) { + const oklchMatch = css.match(/oklch\(([^)]+)\)/); + if (oklchMatch) { + const values = oklchMatch[1].split(/\s+/).map(v => parseFloat(v.trim())); + if (values.length >= 3) { + const [L, C, h] = values; + return { L, C, h }; + } + } + } + + // For non-OKLCH colors, convert to OKLCH for consistency + const rgb = parseCssColorToRgb(css); + if (rgb) { + const { L, a, b } = rgbToOklab(rgb.r, rgb.g, rgb.b); + const { C, h } = oklabToOklch(L, a, b); + return { L, C, h }; + } + return null; + }; + + // Keep getPrimaryHex for backward compatibility, but now it converts from OKLCH + const getPrimaryHex = () => { + const oklch = getPrimaryOKLCH(); + if (!oklch) return null; + + const { a, b } = oklchToOklab(oklch.L, oklch.C, oklch.h); + const rgb = oklabToRgb(oklch.L, a, b); + return toHex(rgb); + }; + // No count management via CSS anymore; counts are passed directly to the API + + const generators = { + categorical: (baseOKLCH, count) => { + const { L, C, h } = baseOKLCH; + const L0 = Math.min(0.85, Math.max(0.4, L)); + const C0 = Math.min(0.35, Math.max(0.1, C || 0.2)); + const total = Math.max(1, Math.min(12, count || 8)); + const hueStep = 360 / total; + const results = []; + for (let i = 0; i < total; i++) { + const hDeg = (h + i * hueStep) % 360; + const lVar = ((i % 3) - 1) * 0.04; + results.push(oklchToHexSafe(Math.max(0.4, Math.min(0.85, L0 + lVar)), C0, hDeg)); + } + return results; + }, + sequential: (baseOKLCH, count) => { + const { L, C, h } = baseOKLCH; + const total = Math.max(1, Math.min(12, count || 8)); + const startL = Math.max(0.25, L - 0.18); + const endL = Math.min(0.92, L + 0.18); + const cBase = Math.min(0.33, Math.max(0.08, C * 0.9 + 0.06)); + const out = []; + for (let i = 0; i < total; i++) { + const t = total === 1 ? 0 : i / (total - 1); + const lNow = startL * (1 - t) + endL * t; + const cNow = cBase * (0.85 + 0.15 * (1 - Math.abs(0.5 - t) * 2)); + out.push(oklchToHexSafe(lNow, cNow, h)); + } + return out; + }, + diverging: (baseOKLCH, count) => { + const { L, C, h } = baseOKLCH; + const total = Math.max(1, Math.min(12, count || 8)); + + // Left endpoint: EXACT primary color (no darkening) + const leftLab = oklchToOklab(L, C, h); + // Right endpoint: complement with same L and similar C (clamped safe) + const compH = (h + 180) % 360; + const cSafe = Math.min(0.35, Math.max(0.08, C)); + const rightLab = oklchToOklab(L, cSafe, compH); + const whiteLab = { L: 0.98, a: 0, b: 0 }; // center near‑white + + const hexFromOKLab = (L, a, b) => toHex(oklabToRgb(L, a, b)); + const lerp = (a, b, t) => a + (b - a) * t; + const lerpOKLabHex = (A, B, t) => hexFromOKLab(lerp(A.L, B.L, t), lerp(A.a, B.a, t), lerp(A.b, B.b, t)); + + const out = []; + if (total % 2 === 1) { + const nSide = (total - 1) >> 1; // items on each side + // Left side: include left endpoint exactly at index 0 + for (let i = 0; i < nSide; i++) { + const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1 + // Move from leftLab to a value close (but not equal) to white; ensure last before center is lighter + const tt = t * 0.9; // keep some distance from pure white before center + out.push(lerpOKLabHex(leftLab, whiteLab, tt)); + } + // Center + out.push(hexFromOKLab(whiteLab.L, whiteLab.a, whiteLab.b)); + // Right side: start near white and end EXACTLY at rightLab + for (let i = 0; i < nSide; i++) { + const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1 + const tt = Math.max(0.1, t); // avoid starting at pure white + out.push(lerpOKLabHex(whiteLab, rightLab, tt)); + } + // Ensure first and last are exact endpoints + if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); } + } else { + const nSide = total >> 1; + // Left half including left endpoint, approaching white but not reaching it + for (let i = 0; i < nSide; i++) { + const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1 + const tt = t * 0.9; + out.push(lerpOKLabHex(leftLab, whiteLab, tt)); + } + // Right half: mirror from near white to exact right endpoint + for (let i = 0; i < nSide; i++) { + const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1 + const tt = Math.max(0.1, t); + out.push(lerpOKLabHex(whiteLab, rightLab, tt)); + } + if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); } + } + return out; + } + }; + + let lastSignature = ''; + + const updatePalettes = () => { + const primaryOKLCH = getPrimaryOKLCH(); + const primaryHex = getPrimaryHex(); + const signature = `${primaryOKLCH?.L},${primaryOKLCH?.C},${primaryOKLCH?.h}`; + if (signature === lastSignature) return; + lastSignature = signature; + try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { } + }; + + const bootstrap = () => { + // Initial setup - only run once on page load + updatePalettes(); + + // Observer will handle all subsequent changes + const mo = new MutationObserver(() => updatePalettes()); + mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] }); + + // Utility: choose high-contrast (or softened) text style against an arbitrary background color + const pickTextStyleForBackground = (bgCss, opts = {}) => { + const cssRoot = document.documentElement; + const getCssVar = (name) => { + try { return getComputedStyle(cssRoot).getPropertyValue(name).trim(); } catch { return ''; } + }; + const resolveCssToRgb01 = (css) => { + const rgb = parseCssColorToRgb(css); + if (!rgb) return null; + return rgb; // already 0..1 + }; + 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 }); + const relLum = (rgb) => { + const f = (u) => srgbToLinear(u); + return 0.2126 * f(rgb.r) + 0.7152 * f(rgb.g) + 0.0722 * f(rgb.b); + }; + const contrast = (fg, bg) => { + const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1, L2), b = Math.min(L1, L2); + return (a + 0.05) / (b + 0.05); + }; + try { + const bg = resolveCssToRgb01(bgCss); + if (!bg) return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 }; + const candidatesCss = [getCssVar('--text-color') || '#111', getCssVar('--on-primary') || '#0f1115', '#000', '#fff']; + const candidates = candidatesCss + .map(css => ({ css, rgb: resolveCssToRgb01(css) })) + .filter(x => !!x.rgb); + // Pick the max contrast + let best = candidates[0]; let bestCR = contrast(best.rgb, bg); + for (let i = 1; i < candidates.length; i++) { + const cr = contrast(candidates[i].rgb, bg); + if (cr > bestCR) { best = candidates[i]; bestCR = cr; } + } + // Optional softening via blend factor (0..1), blending towards muted color + const blend = Math.min(1, Math.max(0, Number(opts.blend || 0))); + let finalRgb = best.rgb; + if (blend > 0) { + const mutedCss = getCssVar('--muted-color') || (getCssVar('--text-color') || '#111'); + const mutedRgb = resolveCssToRgb01(mutedCss) || best.rgb; + finalRgb = mixRgb01(best.rgb, mutedRgb, blend); + } + const haloStrength = Math.min(1, Math.max(0, Number(opts.haloStrength == null ? 0.5 : opts.haloStrength))); + 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})`; + return { fill: toHex(finalRgb), stroke, strokeWidth: (opts.haloWidth == null ? 1 : Number(opts.haloWidth)) }; + } catch { + return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 }; + } + }; + window.ColorPalettes = { + refresh: updatePalettes, + notify: () => { try { const primaryOKLCH = getPrimaryOKLCH(); const primaryHex = getPrimaryHex(); document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { } }, + getPrimary: () => getPrimaryHex(), + getPrimaryOKLCH: () => getPrimaryOKLCH(), + getColors: (key, count = 6) => { + const primaryOKLCH = getPrimaryOKLCH(); + if (!primaryOKLCH) return []; + const total = Math.max(1, Math.min(12, Number(count) || 6)); + if (key === 'categorical') return generators.categorical(primaryOKLCH, total); + if (key === 'sequential') return generators.sequential(primaryOKLCH, total); + if (key === 'diverging') return generators.diverging(primaryOKLCH, total); + return []; + }, + getTextStyleForBackground: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}), + chooseReadableText: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}) + }; + }; + + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once: true }); + else bootstrap(); +})(); + + diff --git a/app/src/components/Footer.astro b/app/src/components/Footer.astro new file mode 100644 index 0000000000000000000000000000000000000000..e94fea7157ed5185f46cc38a190467a9e9e0d67d --- /dev/null +++ b/app/src/components/Footer.astro @@ -0,0 +1,347 @@ +--- +interface Props { + citationText: string; + bibtex: string; + licence?: string; + doi?: string; +} +const { citationText, bibtex, licence, doi } = Astro.props as Props; +--- + +
        + +
        + + + + diff --git a/app/src/components/FullWidth.astro b/app/src/components/FullWidth.astro new file mode 100644 index 0000000000000000000000000000000000000000..0842ddd33c9246dbef1ddd80edc286a934e36fa0 --- /dev/null +++ b/app/src/components/FullWidth.astro @@ -0,0 +1,9 @@ +--- +const { class: className, ...props } = Astro.props; +const wrapperClass = ["full-width", className].filter(Boolean).join(" "); +--- +
        + +
        + + diff --git a/app/src/components/Hero.astro b/app/src/components/Hero.astro new file mode 100644 index 0000000000000000000000000000000000000000..61f09236780d0dac3271c44f97e807b502643399 --- /dev/null +++ b/app/src/components/Hero.astro @@ -0,0 +1,612 @@ +--- +import HtmlEmbed from "./HtmlEmbed.astro"; + +interface Props { + title: string; // may contain HTML (e.g.,
        ) + titleRaw?: string; // plain title for slug/PDF (optional) + description?: string; + authors?: Array< + string | { name: string; url?: string; affiliationIndices?: number[] } + >; + affiliations?: Array<{ id: number; name: string; url?: string }>; + affiliation?: string; // legacy single affiliation + published?: string; + doi?: string; + arxiv?: string; + pdfProOnly?: boolean; // Gate PDF download to Pro users only +} + +const { + title, + titleRaw, + description, + authors = [], + affiliations = [], + affiliation, + published, + doi, + arxiv, + pdfProOnly = false, +} = Astro.props as Props; + +type Author = { name: string; url?: string; affiliationIndices?: number[] }; + +function normalizeAuthors( + input: Array< + | string + | { + name?: string; + url?: string; + link?: string; + affiliationIndices?: number[]; + } + >, +): Author[] { + return (Array.isArray(input) ? input : []) + .map((a) => { + if (typeof a === "string") { + return { name: a } as Author; + } + const name = (a?.name ?? "").toString(); + const url = (a?.url ?? a?.link) as string | undefined; + const affiliationIndices = Array.isArray((a as any)?.affiliationIndices) + ? (a as any).affiliationIndices + : undefined; + return { name, url, affiliationIndices } as Author; + }) + .filter((a) => a.name && a.name.trim().length > 0); +} + +const normalizedAuthors: Author[] = normalizeAuthors(authors as any); + +// Determine if affiliation superscripts should be shown (only when there are multiple distinct affiliations referenced by authors) +const authorAffiliationIndexSet = new Set(); +for (const author of normalizedAuthors) { + const indices = Array.isArray(author.affiliationIndices) + ? author.affiliationIndices + : []; + for (const idx of indices) { + if (typeof idx === "number") { + authorAffiliationIndexSet.add(idx); + } + } +} +const shouldShowAffiliationSupers = authorAffiliationIndexSet.size > 1; +const hasMultipleAffiliations = + Array.isArray(affiliations) && affiliations.length > 1; + +function stripHtml(text: string): string { + return String(text || "").replace(/<[^>]*>/g, ""); +} + +function slugify(text: string): string { + return ( + String(text || "") + .normalize("NFKD") + .replace(/\p{Diacritic}+/gu, "") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 120) || "article" + ); +} + +const pdfBase = titleRaw ? stripHtml(titleRaw) : stripHtml(title); +const pdfFilename = `${slugify(pdfBase)}.pdf`; +--- + +
        +

        +
        + + {description &&

        {description}

        } +
        + +

        + +
        +
        + { + normalizedAuthors.length > 0 && ( +
        +

        Author{normalizedAuthors.length > 1 ? "s" : ""}

        +
          + {normalizedAuthors.map((a, i) => { + const supers = + shouldShowAffiliationSupers && + Array.isArray(a.affiliationIndices) && + a.affiliationIndices.length ? ( + {a.affiliationIndices.join(", ")} + ) : null; + return ( +
        • + {a.url ? {a.name} : a.name}{supers}{i < normalizedAuthors.length - 1 && } +
        • + ); + })} +
        +
        + ) + } + { + Array.isArray(affiliations) && affiliations.length > 0 && ( +
        +

        Affiliation{affiliations.length > 1 ? "s" : ""}

        + {hasMultipleAffiliations ? ( +
          + {affiliations.map((af) => ( +
        1. + {af.url ? ( + + {af.name} + + ) : ( + af.name + )} +
        2. + ))} +
        + ) : ( +

        + {affiliations[0]?.url ? ( + + {affiliations[0].name} + + ) : ( + affiliations[0]?.name + )} +

        + )} +
        + ) + } + { + (!affiliations || affiliations.length === 0) && affiliation && ( +
        +

        Affiliation

        +

        {affiliation}

        +
        + ) + } + { + published && ( +
        +

        Published

        +

        {published}

        +
        + ) + } + +
        +
        +

        PDF

        + + +
        +
        +

        Checking access...

        + + +
        +
        +
        +
        + + + + diff --git a/app/src/components/HtmlEmbed.astro b/app/src/components/HtmlEmbed.astro new file mode 100644 index 0000000000000000000000000000000000000000..73a9e833d6202aca7358bfeffe45ada60a0c8fd7 --- /dev/null +++ b/app/src/components/HtmlEmbed.astro @@ -0,0 +1,407 @@ +--- +interface Props { + src: string; + title?: string; + desc?: string; + caption?: string; + frameless?: boolean; + align?: "left" | "center" | "right"; + id?: string; + data?: string | string[]; + config?: any; +} +const { + src, + title, + desc, + caption, + frameless = false, + align = "left", + id, + data, + config, +} = Astro.props as Props; + +// Load all .html embeds under src/content/embeds/** as strings (dev & build) +const embeds = (import.meta as any).glob("../content/embeds/**/*.html", { + query: "?raw", + import: "default", + eager: true, +}) as Record; + +function resolveFragment(requested: string): string | null { + // Allow both "banner.html" and "embeds/banner.html" + const needle = requested.replace(/^\/*/, ""); + for (const [key, html] of Object.entries(embeds)) { + if ( + key.endsWith("/" + needle) || + key.endsWith("/" + needle.replace(/^embeds\//, "")) + ) { + return html; + } + } + return null; +} + +const html = resolveFragment(src); +const mountId = `frag-${Math.random().toString(36).slice(2)}`; +const dataAttr = Array.isArray(data) + ? JSON.stringify(data) + : typeof data === "string" + ? data + : undefined; +const configAttr = + typeof config === "string" + ? config + : config != null + ? JSON.stringify(config) + : undefined; + +// Apply the ID to the HTML content if provided +const htmlWithId = + id && html + ? html.replace(/
        ]*>/, `
        `) + : html; +--- + +{ + html ? ( +
        + {title && ( +
        + {title} +
        + )} +
        +
        +
        + {(desc || caption) && ( +
        + )} +
        + ) : ( +
        +
        +
        + Embed not found +

        + The requested embed could not be loaded: {src} +

        +
        +
        +
        + ) +} + + + + diff --git a/app/src/components/Note.astro b/app/src/components/Note.astro new file mode 100644 index 0000000000000000000000000000000000000000..f37b1ca0273f636e6aaf064c099031fb8b35a9a0 --- /dev/null +++ b/app/src/components/Note.astro @@ -0,0 +1,125 @@ +--- +interface Props { + title?: string; + emoji?: string; + class?: string; + variant?: "neutral" | "info" | "success" | "danger"; +} +const { + title, + emoji, + class: className, + variant = "neutral", + ...props +} = Astro.props as Props; +const wrapperClass = ["note", `note--${variant}`, className] + .filter(Boolean) + .join(" "); +const hasHeader = + (emoji && String(emoji).length > 0) || (title && String(title).length > 0); +--- + +
        + {title ? ( + +
        +
        + {emoji && } +
        {title}
        +
        +
        + +
        +
        + ) : ( + +
        + { + emoji && ( + + ) + } +
        +
        + +
        +
        +
        + )} +
        + + diff --git a/app/src/components/Quote.astro b/app/src/components/Quote.astro new file mode 100644 index 0000000000000000000000000000000000000000..a1b654a00f2e534de6b25ef076b14116cf6b5afd --- /dev/null +++ b/app/src/components/Quote.astro @@ -0,0 +1,140 @@ +--- +interface Props { + author?: string; + source?: string; +} + +const { author, source } = Astro.props; +--- + +
        +
        + +
        + { + (author || source) && ( +
        + {author && } + {author && source && , } + {source && } +
        + ) + } +
        + + diff --git a/app/src/components/Reference.astro b/app/src/components/Reference.astro new file mode 100644 index 0000000000000000000000000000000000000000..0b9cadceda8321a30effb4d9e8ea848cc59d46e0 --- /dev/null +++ b/app/src/components/Reference.astro @@ -0,0 +1,45 @@ +--- +interface Props { + /** ID unique pour la référence */ + id: string; + /** Légende HTML pour la référence */ + caption: string; +} + +const { id, caption } = Astro.props as Props; +--- + +
        +
        +
        + +
        +
        +
        +
        + + diff --git a/app/src/components/Seo.astro b/app/src/components/Seo.astro new file mode 100644 index 0000000000000000000000000000000000000000..2936462623f0781add973f2aaff6f2776f673cc9 --- /dev/null +++ b/app/src/components/Seo.astro @@ -0,0 +1,51 @@ +--- +interface Props { + title: string; + description?: string; + authors?: string[]; + published?: string; + tags?: string[]; + image?: string; // Preferred absolute URL +} + +const { title, description = '', authors = [], published, tags = [], image } = Astro.props as Props; + +const url = Astro.url?.toString?.() ?? ''; +const site = (Astro.site ? String(Astro.site) : '') as string; + +const ogImage = image && image.length > 0 + ? (image.startsWith('http') ? image : (site ? new URL(image, site).toString() : image)) + : undefined; + +const jsonLd = { + '@context': 'https://schema.org', + '@type': 'Article', + headline: title, + description: description || undefined, + datePublished: published || undefined, + author: authors.map((name) => ({ '@type': 'Person', name })), + keywords: tags.length ? tags.join(', ') : undefined, + mainEntityOfPage: url || undefined, + image: ogImage ? [ogImage] : undefined, +}; +--- +{title} + + + + + +{description && } + +{ogImage && } +{published && } +{authors.map(a => )} + + + +{description && } +{ogImage && } + + + + diff --git a/app/src/components/TableOfContents.astro b/app/src/components/TableOfContents.astro new file mode 100644 index 0000000000000000000000000000000000000000..040ccc8b2aaa3a7d3b80738659e161421fe999d4 --- /dev/null +++ b/app/src/components/TableOfContents.astro @@ -0,0 +1,860 @@ +--- +export interface Props { + tableOfContentAutoCollapse?: boolean; +} +const { tableOfContentAutoCollapse = false } = Astro.props as Props; +--- + + +
        + Table of Contents +
        +
        + + + + diff --git a/app/src/components/ThemeToggle.astro b/app/src/components/ThemeToggle.astro new file mode 100644 index 0000000000000000000000000000000000000000..92a9c5e20797fdf2d663369509c7eefc4df6d5bb --- /dev/null +++ b/app/src/components/ThemeToggle.astro @@ -0,0 +1,117 @@ + + + diff --git a/app/src/components/Wide.astro b/app/src/components/Wide.astro new file mode 100644 index 0000000000000000000000000000000000000000..4223b65525c0942ed78e37c60c450451487d97c9 --- /dev/null +++ b/app/src/components/Wide.astro @@ -0,0 +1,9 @@ +--- +const { class: className, ...props } = Astro.props; +const wrapperClass = ["wide", className].filter(Boolean).join(" "); +--- +
        + +
        + + diff --git a/app/src/content/article.mdx b/app/src/content/article.mdx new file mode 100644 index 0000000000000000000000000000000000000000..4a55f8f74cee72927150bf57b9407d3e06e2882f --- /dev/null +++ b/app/src/content/article.mdx @@ -0,0 +1,332 @@ +--- +seoThumbImage: 'https://seongland.com/article/crl/og.png' +title: 'Control Reinforcement Learning' +subtitle: >- + Interpretable Token-Level Steering of LLMs via SAE Features +description: 'CRL: Interpretable Token-Level Steering of LLMs via Sparse Autoencoder Features' +authors: + - name: Seonglae Cho + affiliations: + - 1 + - 2 + - name: Zekun Wu + affiliations: + - 1 + - 2 + - name: Adriano Koshiyama + affiliations: + - 1 + - 2 +affiliations: + - name: Holistic AI + url: 'https://holisticai.com' + - name: University College London + url: 'https://ucl.ac.uk' +published: 'Feb. 13, 2026' +licence: > + Diagrams and text are licensed under CC‑BY 4.0. +doi: '10.48550/arXiv.2602.10437' +arxiv: '2602.10437' +tags: + - mechanistic interpretability + - sparse autoencoders + - reinforcement learning + - language models + - feature attribution + +tableOfContentsAutoCollapse: true +--- + +import Sidenote from '../components/Sidenote.astro'; +import HtmlEmbed from '../components/HtmlEmbed.astro'; +import Note from '../components/Note.astro'; +import Wide from '../components/Wide.astro'; +import FullWidth from '../components/FullWidth.astro'; +import Quote from '../components/Quote.astro'; +import Reference from '../components/Reference.astro'; + +## Abstract + +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. + +**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]. + +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. + + + + + +## The Problem: Static Feature Analysis Falls Short + +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." + + +SAEs learn: $\mathcal{L} = \|\mathbf{x} - \hat{\mathbf{x}}\|^2 + \lambda \|\mathbf{z}\|_1$ where $\mathbf{z}$ is the sparse feature activation vector. + + +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. + +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?** + + +**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. + + +Current approaches to SAE-based control face limitations: +- **Contrastive methods** require carefully curated positive/negative datasets for each feature +- **[Activation patching](https://texonom.com/11fc3c96247d80d683e6c67f149c992d)** requires extensive storage and computation for exhaustive feature probing +- **Manual coefficient tuning** doesn't generalize across layers or tasks + +CRL addresses these by framing feature selection as a *learned* decision at every token, using reinforcement learning to discover which features are worth amplifying. + + +## How CRL Works + + + + + +### Formulating Steering as an MDP + +CRL treats the control of transformer representations as a **[Markov Decision Process](https://texonom.com/d01c5c23d56648ce843727514008e25c)** over SAE features: + +
        + +$$\tilde{\mathbf{x}}_t = \mathbf{x}_t + c \cdot \mathbf{a}_t \mathbf{W}_{dec}$$ + +
        + +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. + +The MDP components are: + +- **State** $\mathbf{s} = \mathbf{x}$: the residual stream activation at the intervention layer +- **Action** $\mathbf{a} \in \{0,1\}^{d_{dict}}$: a sparse binary vector selecting SAE features (typically $k=1$) +- **Transition**: deterministic, governed by the transformer's forward pass with the steering intervention +- **Reward**: task-specific binary signal (correct/incorrect output) + + +Using $k=1$ (single feature selection) is critical for interpretability — each intervention is attributable to exactly one SAE feature. + + +### The Policy and Critic Networks + + + + + +The **policy network** $\pi_\theta$ is a 2-layer MLP with Tanh activation that maps residual stream activations to feature selection logits: + +$$\boldsymbol{\mu} = \pi_\theta(\mathbf{s}), \quad \mathbf{a} = \text{TopK}(\boldsymbol{\mu}, k)$$ + +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. + +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. + + +**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. + + +### Partial Observability + +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. + + +## Adaptive Feature Masking + +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. + +
        + +$$\text{mask}_i^{(t+1)} = \text{mask}_i^{(t)} \lor \text{active}_i^{(t)}$$ + +
        + +The mask starts small and **progressively expands** as the model generates tokens — features that naturally activate during generation become available for future policy selections: + +1. Policy computes logits and masks unavailable features: $\mathbf{a}_t = \text{TopK}(\boldsymbol{\mu} \odot \text{mask}^{(t)}, k)$ +2. Steering is applied to the residual stream +3. Natural SAE activations at the current token expand the mask for the next step + +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. + + +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. + + + +## Branch Analysis: Where Steering Decides the Answer + +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. + +### How Branch Analysis Works + +For each generation, we track: +1. The **context** (shared prefix before the branch point) +2. The **correct path** — the feature that leads to a correct answer +3. The **incorrect path** — the feature that leads to a wrong answer + +This provides direct causal evidence that specific features are responsible for output quality, not just correlated with it. + +### Layer-Specific Feature Semantics + +Comparing features selected at Layer 10 vs Layer 20 reveals a striking pattern: + +
        +
        +

        Layer 10: Syntactic Features

        +
          +
        • L10/16145 "mathematical notation"
        • +
        • L10/5423 "pronoun references"
        • +
        • L10/8428 "clinical examinations" (surface similarity)
        • +
        • L10/8775 "legal contracts" (terminology match)
        • +
        +
        +
        +

        Layer 20: Semantic Features

        +
          +
        • L10/12607 "mathematical proofs" (logical structure)
        • +
        • L20/11163 "assessments and strategic plans"
        • +
        • L20/2878 "code formatting" (structural patterns)
        • +
        • L20/998 "needs identification" (requirement semantics)
        • +
        +
        +
        + +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. + + +**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. + + +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). + + +## Empirical Validation + +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. + + + +### Learned vs. Random Selection + + + + + +| Method | BBQ Ambig | HarmBench | +|--------|-----------|-----------| +| No Steering (Base) | 60.17 | 41.46 | +| Random Feature | 58.36 | 45.35 | +| Random + AFM | 60.16 | 46.96 | +| Most-Active Feature | 59.94 | 48.03 | +| **CRL (Ours)** | **65.86** | **49.12** | + +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. + + +## Critic Trajectory Analysis + +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. + + + + + +### Key Observations + +For multi-token generation (GSM8K), critic trajectories reveal the *temporal dynamics* of steering: + +- **Correct samples** (green): Higher gradients with divergence around token 200–400 +- **Incorrect samples** (red): Flat or declining values throughout +- **Corrected samples** (blue): Show value increases at the ~400-token point, indicating interventions become more effective in later reasoning stages +- **Misguided samples** (yellow): Initially high values that drop — the steering seemed promising but ultimately led astray + + +## Feature Interpretability + +CRL provides per-token intervention logs that reveal *which* features are selected and *when*. This enables a new form of mechanistic analysis. + +### Feature-Token Alignment + + + + + +**Corrected examples** show clear semantic coherence: features activate on mathematical tokens, operators, and quantitative units -- demonstrating lexical generalization beyond surface forms. + +### When Features Fail + +**Misguided examples** (select the "Misguided" tab above) reveal failure modes: +- Task-irrelevant features disrupting the reasoning chain +- Token-feature misalignment producing incoherent activations +- Feature interference causing the model to abandon its solution path + +### Feature Diversity + + + +Feature diversity varies significantly across tasks, measured by entropy of the feature selection distribution: + +| Task | Feature Diversity | Interpretation | +|------|------------------|----------------| +| GSM8K | 6.695 | Complex reasoning needs diverse features | +| MMLU-Pro | 5.476 | High complexity, broad engagement | +| XSTest | 4.757 | Intermediate diversity | +| HarmBench | 2.938 | Focused refusal uses concentrated features | + +Complex reasoning tasks require *broader* feature engagement, while focused behavioral tasks benefit from *concentrated* feature activation. + + +## Layer-Wise Analysis + +Different layers encode different types of information, and CRL's layer-wise training reveals this structure. + + + + + +### Residual Stream Norms + + + +Residual stream norms increase with depth, which directly impacts the optimal steering coefficient: +- **Early layers** (L5–L10): $c = 10\text{–}20$ +- **Middle layers** (L15–L20): $c = 30\text{–}60$ +- **Late layers** (L22–L25): $c = 60\text{–}100$ + +Large coefficients degrade early-layer performance, while late layers tolerate — and require — stronger interventions. + + +## Discussion + +### What CRL Reveals as an Analysis Tool + +CRL provides three diagnostic capabilities that complement static feature analysis: + +**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. + +**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. + +**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). + +### Limitations + +- **Layer selection is post-hoc:** optimal layers are identified via validation, not predicted a priori +- **Feature labels are unreliable:** SAE feature descriptions often diverge from their actual steering effects +- **Single-feature constraint:** selecting $k=1$ maximizes interpretability but may leave performance on the table +- **Computational cost:** while modest (8–9% overhead), CRL requires PPO training for each task-layer combination + + +## Conclusion + +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: + +- **Branch tracking** locates critical decision points where feature choice determines output correctness +- **Critic trajectories** separate policy limitations from value-estimation errors +- **Layer comparison** reveals syntactic-to-semantic feature gradients across network depth + +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. diff --git a/app/src/content/assets/data/bbq-ambig-layer-sweep.json b/app/src/content/assets/data/bbq-ambig-layer-sweep.json new file mode 100644 index 0000000000000000000000000000000000000000..09c0995d442c9c456329c820fb972c8c548e8caa --- /dev/null +++ b/app/src/content/assets/data/bbq-ambig-layer-sweep.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99f3cada76e74b16d3d4fdc66f320359a7f2f5844bb2eb787343f5dff734bc5f +size 3136 diff --git a/app/src/content/assets/data/bbq-disambig-layer-sweep.json b/app/src/content/assets/data/bbq-disambig-layer-sweep.json new file mode 100644 index 0000000000000000000000000000000000000000..79137b478ef1db89f370b3c3043ee0ff5d21db5b --- /dev/null +++ b/app/src/content/assets/data/bbq-disambig-layer-sweep.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbdcdb11ef584f5a95bc6213ac100391859a15ab938c7e25d4482b809663175b +size 2264 diff --git a/app/src/content/assets/data/mmlu-layer-sweep-constrained.json b/app/src/content/assets/data/mmlu-layer-sweep-constrained.json new file mode 100644 index 0000000000000000000000000000000000000000..7d3dafbf4d1a431acd4767d9aec1b7d3a6e06a32 --- /dev/null +++ b/app/src/content/assets/data/mmlu-layer-sweep-constrained.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2173d7c14090dcf34446f00266a99e719112e4068500a6baf0af57ba09cad9ca +size 2880 diff --git a/app/src/content/assets/data/mmlu-layer-sweep.json b/app/src/content/assets/data/mmlu-layer-sweep.json new file mode 100644 index 0000000000000000000000000000000000000000..e9ebf63f5a35fb91e16548d24f8570588b9368c9 --- /dev/null +++ b/app/src/content/assets/data/mmlu-layer-sweep.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfc679c136321b7cda6490f334e8cf132bafac1a649a4f1698af018c13800ddf +size 7423 diff --git a/app/src/content/assets/image/flowchart.png b/app/src/content/assets/image/flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..de66401e4e0a27519235168518f4ad23f1ee9c84 --- /dev/null +++ b/app/src/content/assets/image/flowchart.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af692a05baf908a971a388b758e19cad74005880d3319f581afbb64c6839a913 +size 578257 diff --git a/app/src/content/bibliography.bib b/app/src/content/bibliography.bib new file mode 100644 index 0000000000000000000000000000000000000000..6ef19cf368164004be72a2f966cd9a9ac568e511 --- /dev/null +++ b/app/src/content/bibliography.bib @@ -0,0 +1,1739 @@ +% LLM +@misc{nikolaou2025languagemodelsinjectiveinvertible, + title={Language Models are Injective and Hence Invertible}, + author={Giorgos Nikolaou and Tommaso Mencattini and Donato Crisostomi and Andrea Santilli and Yannis Panagakis and Emanuele Rodolà}, + year={2025}, + eprint={2510.15511}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2510.15511}, +} + +@inproceedings{vaswani2017attention, + 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}, + booktitle = {Advances in Neural Information Processing Systems}, + editor = {I. Guyon and U. Von Luxburg and S. Bengio and H. Wallach and R. Fergus and S. Vishwanathan and R. Garnett}, + pages = {}, + publisher = {Curran Associates, Inc.}, + title = {Attention is All you Need}, + url = {https://proceedings.neurips.cc/paper_files/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf}, + volume = {30}, + year = {2017} +} + +@inproceedings{wei2022finetuned, + title={Finetuned Language Models are Zero-Shot Learners}, + 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}, + booktitle={International Conference on Learning Representations}, + year={2022}, + url={https://openreview.net/forum?id=gEZrGCozdqR} +} + +@article{wei2022emergent, + title={Emergent Abilities of Large Language Models}, + 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}, + journal={Transactions on Machine Learning Research}, + issn={2835-8856}, + year={2022}, + url={https://openreview.net/forum?id=yzkSU5zdwD}, + note={Survey Certification} +} + +@inproceedings{parikh-etal-2016-decomposable, + title = {A Decomposable Attention Model for Natural Language Inference}, + author = {Parikh, Ankur and + T{\"a}ckstr{\"o}m, Oscar and + Das, Dipanjan and + Uszkoreit, Jakob}, + editor = {Su, Jian and + Duh, Kevin and + Carreras, Xavier}, + booktitle = {Proceedings of the 2016 Conference on Empirical Methods in Natural Language Processing}, + month = nov, + year = {2016}, + address = {Austin, Texas}, + publisher = {Association for Computational Linguistics}, + url = {https://aclanthology.org/D16-1244/}, + doi = {10.18653/v1/D16-1244}, + pages = {2249--2255} +} + +@inproceedings{kojima2022large, + title={Large Language Models are Zero-Shot Reasoners}, + author={Takeshi Kojima and Shixiang Shane Gu and Machel Reid and Yutaka Matsuo and Yusuke Iwasawa}, + booktitle={Advances in Neural Information Processing Systems}, + editor={Alice H. Oh and Alekh Agarwal and Danielle Belgrave and Kyunghyun Cho}, + year={2022}, + url={https://openreview.net/forum?id=e2TBb5y0yFf} +} + +@inproceedings{bahdanau2015neural, + author = {Dzmitry Bahdanau and + Kyunghyun Cho and + Yoshua Bengio}, + editor = {Yoshua Bengio and + Yann LeCun}, + title = {Neural Machine Translation by Jointly Learning to Align and Translate}, + booktitle = {3rd International Conference on Learning Representations, {ICLR} 2015, + San Diego, CA, USA, May 7-9, 2015, Conference Track Proceedings}, + year = {2015}, + url = {http://arxiv.org/abs/1409.0473}, + timestamp = {Wed, 17 Jul 2019 10:40:54 +0200}, + biburl = {https://dblp.org/rec/journals/corr/BahdanauCB14.bib}, + bibsource = {dblp computer science bibliography, https://dblp.org} +} + +% Training +@inproceedings{hu2022lora, + title={Lo{RA}: Low-Rank Adaptation of Large Language Models}, + 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}, + booktitle={International Conference on Learning Representations}, + year={2022}, + url={https://openreview.net/forum?id=nZeVKeeFYf9} +} + +% Inference-time +@inproceedings{wei2022chain, + title={Chain of Thought Prompting Elicits Reasoning in Large Language Models}, + 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}, + booktitle={Advances in Neural Information Processing Systems}, + editor={Alice H. Oh and Alekh Agarwal and Danielle Belgrave and Kyunghyun Cho}, + year={2022}, + url={https://openreview.net/forum?id=\_VjQlMeSB\_J} +} + +@misc{hu2024understanding, + title={Understanding Reasoning in Chain-of-Thought from the Hopfieldian View}, + 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}, + year={2024}, + url={https://openreview.net/forum?id=OclSRDktp3} +} + +@inproceedings{zhao-etal-2024-enhancing-zero, + title = "Enhancing Zero-Shot Chain-of-Thought Reasoning in Large Language Models through Logic", + author = "Zhao, Xufeng and + Li, Mengdi and + Lu, Wenhao and + Weber, Cornelius and + Lee, Jae Hee and + Chu, Kun and + Wermter, Stefan", + editor = "Calzolari, Nicoletta and + Kan, Min-Yen and + Hoste, Veronique and + Lenci, Alessandro and + Sakti, Sakriani and + Xue, Nianwen", + booktitle = "Proceedings of the 2024 Joint International Conference on Computational Linguistics, Language Resources and Evaluation (LREC-COLING 2024)", + month = may, + year = "2024", + address = "Torino, Italia", + publisher = "ELRA and ICCL", + url = "https://aclanthology.org/2024.lrec-main.543/", + pages = "6144--6166" +} + +@inproceedings{li2023inferencetime, + title={Inference-Time Intervention: Eliciting Truthful Answers from a Language Model}, + author={Kenneth Li and Oam Patel and Fernanda Vi{\'e}gas and Hanspeter Pfister and Martin Wattenberg}, + booktitle={Thirty-seventh Conference on Neural Information Processing Systems}, + year={2023}, + url={https://openreview.net/forum?id=aLLuYpn83y} +} + +@article{dutta2024how, + title={How to think step-by-step: A mechanistic understanding of chain-of-thought reasoning}, + author={Subhabrata Dutta and Joykirat Singh and Soumen Chakrabarti and Tanmoy Chakraborty}, + journal={Transactions on Machine Learning Research}, + issn={2835-8856}, + year={2024}, + url={https://openreview.net/forum?id=uHLDkQVtyC}, + note={} +} + + +% Mechanistic Interpretability +@article{olah2017feature, + author = {Olah, Chris and Mordvintsev, Alexander and Schubert, Ludwig}, + title = {Feature Visualization}, + journal = {Distill}, + year = {2017}, + url = {https://distill.pub/2017/feature-visualization}, + doi = {10.23915/distill.00007} +} + +@article{goh2021multimodal, + author = {Goh, Gabriel and Cammarata, Nick and Voss, Chelsea and Carter, Shan and Petrov, Michael and Schubert, Ludwig and Radford, Alec and Olah, Chris}, + title = {Multimodal Neurons in Artificial Neural Networks}, + journal = {Distill}, + year = {2021}, + url = {https://distill.pub/2021/multimodal-neurons}, + doi = {10.23915/distill.00030} +} + +@article{schubert2021high-low, + author = {Schubert, Ludwig and Voss, Chelsea and Cammarata, Nick and Goh, Gabriel and Olah, Chris}, + title = {High-Low Frequency Detectors}, + journal = {Distill}, + year = {2021}, + url = {https://distill.pub/2020/circuits/frequency-edges}, + doi = {10.23915/distill.00024.005} +} + +@article{carter2019activation, + author = {Carter, Shan and Armstrong, Zan and Schubert, Ludwig and Johnson, Ian and Olah, Chris}, + title = {Activation Atlas}, + journal = {Distill}, + year = {2019}, + url = {https://distill.pub/2019/activation-atlas}, + doi = {10.23915/distill.00015} +} + +@inproceedings{oikarinen2025evaluating, + title={Evaluating Neuron Explanations: A Unified Framework with Sanity Checks}, + author={Oikarinen, Tuomas and Yan, Ge and Weng, {Tsui-Wei}}, + booktitle={Forty-second International Conference on Machine Learning}, + year={2025}, + publisher={PMLR}, + url={https://openreview.net/forum?id=8unyWZ14mf} +} + +@misc{cloud2024gradientroutingmaskinggradients, + title={Gradient Routing: Masking Gradients to Localize Computation in Neural Networks}, + author={Alex Cloud and Jacob Goldman-Wetzler and Evžen Wybitul and Joseph Miller and Alexander Matt Turner}, + year={2024}, + eprint={2410.04332}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2410.04332}, +} + +@misc{choi2024automatic, + author = {Choi, Dami and Huang, Vincent and Meng, Kevin and Johnson, Daniel D and Steinhardt, Jacob and Schwettmann, Sarah}, + title = {Scaling Automatic Neuron Description}, + year = {2024}, + month = {October}, + day = {23}, + howpublished = {\url{https://transluce.org/neuron-descriptions}} +} + +@article{olsson2022context, + title={In-context Learning and Induction Heads}, + 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}, + year={2022}, + journal={Transformer Circuits Thread}, + note={https://transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html} +} + +@article{mikolov2013linguistic, + title={Efficient Estimation of Word Representations in Vector Space}, + author={Mikolov, Tomas and Chen, Kai and Corrado, Greg and Dean, Jeffrey}, + journal={arXiv preprint arXiv:1301.3781}, + year={2013}, + url={https://arxiv.org/abs/1301.3781} +} + + +% Circuit +@article{olah2020zoom, + author = {Olah, Chris and Cammarata, Nick and Schubert, Ludwig and Goh, Gabriel and Petrov, Michael and Carter, Shan}, + title = {Zoom In: An Introduction to Circuits}, + journal = {Distill}, + year = {2020}, + note = {https://distill.pub/2020/circuits/zoom-in}, + doi = {10.23915/distill.00024.001} +} + +@article{elhage2021mathematical, + title={A Mathematical Framework for Transformer Circuits}, + 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}, + year={2021}, + journal={Transformer Circuits Thread}, + note={https://transformer-circuits.pub/2021/framework/index.html} +} +@inproceedings{marks2025sparse, +title={Sparse Feature Circuits: Discovering and Editing Interpretable Causal Graphs in Language Models}, +author={Samuel Marks and Can Rager and Eric J Michaud and Yonatan Belinkov and David Bau and Aaron Mueller}, +booktitle={The Thirteenth International Conference on Learning Representations}, +year={2025}, +url={https://openreview.net/forum?id=I4e82CIDxv} +} + + +@article{lindsey2025landscape, + 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}, + title={The Circuits Research Landscape: Results and Perspectives}, + journal={Neuronpedia}, + year={2025}, + url={https://neuronpedia.org/graph/info} +} + +@inproceedings{ferrao2025fsrl, +title={The Anatomy of Alignment: Decomposing Preference Optimization by Steering Sparse Features}, +author={Jeremias Lino Ferrao and Matthijs van der Lende and Ilija Lichkovski and Clement Neo}, +booktitle={Mechanistic Interpretability Workshop at NeurIPS 2025}, +year={2025}, +url={https://openreview.net/forum?id=CoaaltVlx7} +} + +@inproceedings{sinii2025small, +title={Small Vectors, Big Effects: A Mechanistic Study of {RL}-Induced Reasoning via Steering Vectors}, +author={Viacheslav Sinii and Nikita Balagansky and Yaroslav Aksenov and Vadim Kurochkin and Daniil Laptev and Alexey Gorbatovski and Boris Shaposhnikov and Daniil Gavrilov}, +booktitle={Mechanistic Interpretability Workshop at NeurIPS 2025}, +year={2025}, +url={https://openreview.net/forum?id=M8WDG1TfBb} +} + +@inproceedings{tenney-etal-2019-bert, + title = "{BERT} Rediscovers the Classical {NLP} Pipeline", + author = "Tenney, Ian and + Das, Dipanjan and + Pavlick, Ellie", + editor = "Korhonen, Anna and + Traum, David and + M{\`a}rquez, Llu{\'i}s", + booktitle = "Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics", + month = jul, + year = "2019", + address = "Florence, Italy", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/P19-1452/", + doi = "10.18653/v1/P19-1452", + pages = "4593--4601", + 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." +} + + +@inproceedings{cho2025corrsteer, +title={CorrSteer: Generation-Time {LLM} Steering via Correlated Sparse Autoencoder Features}, +author={Cho, Seonglae and Wu, Zekun and Koshiyama, Adriano}, +booktitle={Forty-third International Conference on Machine Learning}, +year={2026} +} + +@inproceedings{conmy2023towards, +title={Towards Automated Circuit Discovery for Mechanistic Interpretability}, +author={Arthur Conmy and Augustine N. Mavor-Parker and Aengus Lynch and Stefan Heimersheim and Adri{\`a} Garriga-Alonso}, +booktitle={Thirty-seventh Conference on Neural Information Processing Systems}, +year={2023}, +url={https://openreview.net/forum?id=89ia77nZ8u} +} + +@article{lindsey2025biology, + 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}, + title={On the Biology of a Large Language Model}, + journal={Transformer Circuits Thread}, + year={2025}, + url={https://transformer-circuits.pub/2025/attribution-graphs/biology.html} +} + +@article{ameisen2025circuit, + 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}, + title={Circuit Tracing: Revealing Computational Graphs in Language Models}, + journal={Transformer Circuits Thread}, + year={2025}, + url={https://transformer-circuits.pub/2025/attribution-graphs/methods.html} +} + +@inproceedings{sun-2025-circuit, + title = "Circuit Stability Characterizes Language Model Generalization", + author = "Sun, Alan", + editor = "Che, Wanxiang and + Nabende, Joyce and + Shutova, Ekaterina and + Pilehvar, Mohammad Taher", + booktitle = "Proceedings of the 63rd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)", + month = jul, + year = "2025", + address = "Vienna, Austria", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2025.acl-long.442/", + doi = "10.18653/v1/2025.acl-long.442", + pages = "9025--9040", + ISBN = "979-8-89176-251-0", +} + +@inproceedings{syed-etal-2024-attribution, + title = "Attribution Patching Outperforms Automated Circuit Discovery", + author = "Syed, Aaquib and + Rager, Can and + Conmy, Arthur", + editor = "Belinkov, Yonatan and + Kim, Najoung and + Jumelet, Jaap and + Mohebbi, Hosein and + Mueller, Aaron and + Chen, Hanjie", + booktitle = "Proceedings of the 7th BlackboxNLP Workshop: Analyzing and Interpreting Neural Networks for NLP", + month = nov, + year = "2024", + address = "Miami, Florida, US", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2024.blackboxnlp-1.25/", + doi = "10.18653/v1/2024.blackboxnlp-1.25", + pages = "407--416", +} + + +% Superposition +@article{elhage2022superposition, + title = {Toy Models of Superposition}, + 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}, + year = {2022}, + journal = {Transformer Circuits Thread}, + url = {https://transformer-circuits.pub/2022/toy\_model/index.html} +} + +@ARTICLE{dongho2006compressed, + author={Donoho, D.L.}, + journal={IEEE Transactions on Information Theory}, + title={Compressed sensing}, + year={2006}, + volume={52}, + number={4}, + pages={1289-1306}, + 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}, + doi={10.1109/TIT.2006.871582} +} + +@inproceedings{bora2017compressed, + title={Compressed Sensing using Generative Models}, + author={Bora, Ashish and Jalal, Ajil and Price, Eric and Dimakis, Alexandros G}, + booktitle={Proceedings of the 34th International Conference on Machine Learning (ICML)}, + editor={Doina Precup and Yee Whye Teh}, + volume={70}, + series={Proceedings of Machine Learning Research}, + pages={537--546}, + year={2017}, + publisher={PMLR}, + address={Sydney, Australia}, + url={https://proceedings.mlr.press/v70/bora17a/bora17a.pdf} +} + + +% Linear Representation +@inproceedings{faruqui-etal-2015-sparse, + title = {Sparse Overcomplete Word Vector Representations}, + author = {Faruqui, Manaal and + Tsvetkov, Yulia and + Yogatama, Dani and + Dyer, Chris and + Smith, Noah A.}, + editor = {Zong, Chengqing and + Strube, Michael}, + 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)}, + month = jul, + year = {2015}, + address = {Beijing, China}, + publisher = {Association for Computational Linguistics}, + url = {https://aclanthology.org/P15-1144/}, + doi = {10.3115/v1/P15-1144}, + pages = {1491--1500} +} + +@inproceedings{socher-etal-2013-recursive, + title = "Recursive Deep Models for Semantic Compositionality Over a Sentiment Treebank", + author = "Socher, Richard and + Perelygin, Alex and + Wu, Jean and + Chuang, Jason and + Manning, Christopher D. and + Ng, Andrew and + Potts, Christopher", + editor = "Yarowsky, David and + Baldwin, Timothy and + Korhonen, Anna and + Livescu, Karen and + Bethard, Steven", + booktitle = "Proceedings of the 2013 Conference on Empirical Methods in Natural Language Processing", + month = oct, + year = "2013", + address = "Seattle, Washington, USA", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/D13-1170/", + pages = "1631--1642" +} + +@article{arora-etal-2018-linear, + title = {Linear Algebraic Structure of Word Senses, with Applications to Polysemy}, + author = {Arora, Sanjeev and + Li, Yuanzhi and + Liang, Yingyu and + Ma, Tengyu and + Risteski, Andrej}, + editor = {Lee, Lillian and + Johnson, Mark and + Toutanova, Kristina and + Roark, Brian}, + journal = {Transactions of the Association for Computational Linguistics}, + volume = {6}, + year = {2018}, + address = {Cambridge, MA}, + publisher = {MIT Press}, + url = {https://aclanthology.org/Q18-1034/}, + doi = {10.1162/tacl_a_00034}, + pages = {483--495} +} + +@inproceedings{park2023the, + title={The Linear Representation Hypothesis and the Geometry of Large Language Models}, + author={Kiho Park and Yo Joong Choe and Victor Veitch}, + booktitle={Causal Representation Learning Workshop at NeurIPS 2023}, + year={2023}, + url={https://openreview.net/forum?id=T0PoOJg8cK} +} + +@inproceedings{park2024the, + title={The Geometry of Categorical and Hierarchical Concepts in Large Language Models}, + author={Kiho Park and Yo Joong Choe and Yibo Jiang and Victor Veitch}, + booktitle={ICML 2024 Workshop on Mechanistic Interpretability}, + year={2024}, + url={https://openreview.net/forum?id=KXuYjuBzKo} +} + + +% Steering Vector +@inproceedings{subramani-etal-2022-extracting, + title = "Extracting Latent Steering Vectors from Pretrained Language Models", + author = "Subramani, Nishant and + Suresh, Nivedita and + Peters, Matthew", + editor = "Muresan, Smaranda and + Nakov, Preslav and + Villavicencio, Aline", + booktitle = "Findings of the Association for Computational Linguistics: ACL 2022", + month = may, + year = "2022", + address = "Dublin, Ireland", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2022.findings-acl.48/", + doi = "10.18653/v1/2022.findings-acl.48", + pages = "566--581" +} + +@misc{azizi2025activationsteeringchainofthoughtcompression, + title={Activation Steering for Chain-of-Thought Compression}, + author={Seyedarmin Azizi and Erfan Baghaei Potraghloo and Massoud Pedram}, + year={2025}, + eprint={2507.04742}, + archivePrefix={arXiv}, + primaryClass={cs.AI}, + url={https://arxiv.org/abs/2507.04742}, +} + +@inproceedings{tan2024analysing, + title={Analysing the Generalisation and Reliability of Steering Vectors}, + 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}, + booktitle={The Thirty-eighth Annual Conference on Neural Information Processing Systems}, + year={2024}, + url={https://openreview.net/forum?id=v8X70gTodR} +} + +@inproceedings{qiu2024spectral, + title={Spectral Editing of Activations for Large Language Model Alignment}, + author={Yifu QIU and Zheng Zhao and Yftah Ziser and Anna Korhonen and Edoardo Ponti and Shay B Cohen}, + booktitle={The Thirty-eighth Annual Conference on Neural Information Processing Systems}, + year={2024}, + url={https://openreview.net/forum?id=pqYceEa87j} +} + + +@inproceedings{luo2024pace, + title={Pa{CE}: Parsimonious Concept Engineering for Large Language Models}, + author={Jinqi Luo and Tianjiao Ding and Kwan Ho Ryan Chan and Darshan Thaker and Aditya Chattopadhyay and Chris Callison-Burch and Rene Vidal}, + booktitle={The Thirty-eighth Annual Conference on Neural Information Processing Systems}, + year={2024}, + url={https://openreview.net/forum?id=lOMHt16T8R} +} + + +@inproceedings{rimsky-etal-2024-steering, + title = "Steering Llama 2 via Contrastive Activation Addition", + author = "Rimsky, Nina and + Gabrieli, Nick and + Schulz, Julian and + Tong, Meg and + Hubinger, Evan and + Turner, Alexander", + editor = "Ku, Lun-Wei and + Martins, Andre and + Srikumar, Vivek", + booktitle = "Proceedings of the 62nd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)", + month = aug, + year = "2024", + address = "Bangkok, Thailand", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2024.acl-long.828/", + doi = "10.18653/v1/2024.acl-long.828", + pages = "15504--15522" +} + +@inproceedings{hernandez2024inspecting, + title={Inspecting and Editing Knowledge Representations in Language Models}, + author={Evan Hernandez and Belinda Z. Li and Jacob Andreas}, + booktitle={First Conference on Language Modeling}, + year={2024}, + url={https://openreview.net/forum?id=ADtL6fgNRv} +} + +@article{DBLP:journals/corr/abs-2308-10248, + publtype={informal}, + author={Alexander Matt Turner and Lisa Thiergart and David Udell and Gavin Leech and Ulisse Mini and Monte MacDiarmid}, + title={Activation Addition: Steering Language Models Without Optimization}, + year={2023}, + cdate={1672531200000}, + journal={CoRR}, + volume={abs/2308.10248}, + url={https://doi.org/10.48550/arXiv.2308.10248} +} + +@inproceedings{konen-etal-2024-style, + title = {Style Vectors for Steering Generative Large Language Models}, + author = {Konen, Kai and + Jentzsch, Sophie and + Diallo, Diaoul{\'e} and + Sch{\"u}tt, Peer and + Bensch, Oliver and + El Baff, Roxanne and + Opitz, Dominik and + Hecking, Tobias}, + editor = {Graham, Yvette and + Purver, Matthew}, + booktitle = {Findings of the Association for Computational Linguistics: EACL 2024}, + month = mar, + year = {2024}, + address = {St. Julian{'}s, Malta}, + publisher = {Association for Computational Linguistics}, + url = {https://aclanthology.org/2024.findings-eacl.52/}, + pages = {782--802} +} + +% Attention Sink +@inproceedings{xiao2024efficient, + title={Efficient Streaming Language Models with Attention Sinks}, + author={Guangxuan Xiao and Yuandong Tian and Beidi Chen and Song Han and Mike Lewis}, + booktitle={The Twelfth International Conference on Learning Representations}, + year={2024}, + url={https://openreview.net/forum?id=NG7sS51zVF} +} + +@article{hazra2025under, + title={Under the Hood of a Reasoning Model}, + 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}, + journal={Goodfire Research Blog}, + year={2025}, + month={Apr}, + day={15}, + note={\url{https://goodfire.ai/blog/under-the-hood-of-a-reasoning-model}}, +} + +@inproceedings{gu2025when, + title={When Attention Sink Emerges in Language Models: An Empirical View}, + author={Xiangming Gu and Tianyu Pang and Chao Du and Qian Liu and Fengzhuo Zhang and Cunxiao Du and Ye Wang and Min Lin}, + booktitle={The Thirteenth International Conference on Learning Representations}, + year={2025}, + url={https://openreview.net/forum?id=78Nn4QJTEN} +} + + +% Sparse Coding +@article{olshausen1997sparse, + title={Sparse coding with an overcomplete basis set: A strategy employed by V1?}, + author={Olshausen, Bruno A. and Field, David J.}, + journal={Vision Research}, + volume={37}, + number={23}, + pages={3311--3325}, + year={1997}, + publisher={Elsevier} +} + +@book{elad2010sparse, + title={Sparse and Redundant Representations: From Theory to Applications in Signal and Image Processing}, + author={Elad, Michael}, + year={2010}, + publisher={Springer New York}, + doi={10.1007/978-1-4419-7011-4}, + isbn={978-1-4419-7010-7}, + edition={1}, + pages={XX, 376} +} + +@article{braun2024identifying, + title={Identifying functionally important features with end-to-end sparse dictionary learning}, + author={Braun, Dan and Taylor, Jordan and Goldowsky-Dill, Nicholas and Sharkey, Lee}, + journal={Advances in Neural Information Processing Systems}, + volume={37}, + pages={107286--107325}, + year={2024} +} + + +% SAE +@inproceedings{huben2024sparse, + title = {Sparse Autoencoders Find Highly Interpretable Features in Language Models}, + author = {Robert Huben and Hoagy Cunningham and Logan Riggs Smith and Aidan Ewart and Lee Sharkey}, + booktitle = {The Twelfth International Conference on Learning Representations}, + year = {2024}, + url = {https://openreview.net/forum?id=F76bwRSLeK} +} + +@inproceedings{leask2025inferencetime, + title={Inference-Time Decomposition of Activations ({ITDA}): A Scalable Approach to Interpreting Large Language Models}, + author={Patrick Leask and Neel Nanda and Noura Al Moubayed}, + booktitle={Forty-second International Conference on Machine Learning}, + year={2025}, + url={https://openreview.net/forum?id=4WMZqAoYi4} +} + +@article{templeton2024scaling, + title = {Scaling Monosemanticity: Extracting Interpretable Features from Claude 3 Sonnet}, + 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}, + year = {2024}, + journal = {Transformer Circuits Thread}, + url = {https://transformer-circuits.pub/2024/scaling-monosemanticity/index.html} +} + +@article{bricken2023monosemanticity, + title={Towards Monosemanticity: Decomposing Language Models With Dictionary Learning}, + 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}, + year={2023}, + journal={Transformer Circuits Thread}, + note={https://transformer-circuits.pub/2023/monosemantic-features/index.html} +} + +@misc{smith2025negative, + title = {Negative Results for SAEs On Downstream Tasks and Deprioritising SAE Research}, + 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}, + year = {2025}, + note = {DeepMind Mechanistic Interpretability Team Progress Update \#2}, + howpublished = {\url{https://www.lesswrong.com/posts/4uXCAJNuPKtKBsi28/sae-progress-update-2-draft}}, +} + +@misc{kissane2024saefinetuning, + author = {Connor Kissane and Robert Krzyzanowski and Arthur Conmy and Neel Nanda}, + title = {SAEs (usually) Transfer Between Base and Chat Models}, + year = {2024}, + howpublished = {Alignment Forum}, + url = {https://www.alignmentforum.org/posts/fmwk6qxrpW8d4jvbd/saes-usually-transfer-between-base-and-chat-models} +} + +@inproceedings{ayonrinde2024interpretability, + title={Interpretability as Compression: Reconsidering {SAE} Explanations of Neural Activations}, + author={Kola Ayonrinde and Michael T Pearce and Lee Sharkey}, + booktitle={NeurIPS 2024 Workshop on Scientific Methods for Understanding Deep Learning}, + year={2024}, + url={https://openreview.net/forum?id=hAqeEZRVSD} +} + +@inproceedings{lieberum-etal-2024-gemma, + title = "Gemma Scope: Open Sparse Autoencoders Everywhere All At Once on Gemma 2", + author = "Lieberum, Tom and + Rajamanoharan, Senthooran and + Conmy, Arthur and + Smith, Lewis and + Sonnerat, Nicolas and + Varma, Vikrant and + Kramar, Janos and + Dragan, Anca and + Shah, Rohin and + Nanda, Neel", + editor = "Belinkov, Yonatan and + Kim, Najoung and + Jumelet, Jaap and + Mohebbi, Hosein and + Mueller, Aaron and + Chen, Hanjie", + booktitle = "Proceedings of the 7th BlackboxNLP Workshop: Analyzing and Interpreting Neural Networks for NLP", + month = nov, + year = "2024", + address = "Miami, Florida, US", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2024.blackboxnlp-1.19/", + doi = "10.18653/v1/2024.blackboxnlp-1.19", + pages = "278--300" +} + +@misc{he2024llamascopeextractingmillions, + title={Llama Scope: Extracting Millions of Features from Llama-3.1-8B with Sparse Autoencoders}, + 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}, + year={2024}, + eprint={2410.20526}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2410.20526}, +} + +@misc{sae_finetuning, + author= {Connor Kissane and Robert Krzyzanowski and Arthur Conmy and Neel Nanda}, + url = {https://www.alignmentforum.org/posts/fmwk6qxrpW8d4jvbd/saes-usually-transfer-between-base-and-chat-models}, + year = {2024}, + howpublished = {Alignment Forum}, + title = {SAEs (usually) Transfer Between Base and Chat Models}, +} + +@misc{stefanhex2023residual, + title = {Residual stream norms grow exponentially over the forward pass}, + author = {Stefan Heimersheim and Alex Turner}, + year = {2023}, + month = {May}, + day = {7}, + url = {https://www.lesswrong.com/posts/8mizBCm3dyc432nK8/residual-stream-norms-grow-exponentially-over-the-forward}, + note = {LessWrong, AI Alignment Forum} +} + + +% SAE Architecture +@inproceedings{bussmann2024batchtopk, + title={BatchTopK Sparse Autoencoders}, + author={Bart Bussmann and Patrick Leask and Neel Nanda}, + booktitle={NeurIPS 2024 Workshop on Scientific Methods for Understanding Deep Learning}, + year={2024}, + url={https://openreview.net/forum?id=d4dpOCqybL} +} + +@misc{rajamanoharan2024jumpingaheadimprovingreconstruction, + title={Jumping Ahead: Improving Reconstruction Fidelity with JumpReLU Sparse Autoencoders}, + author={Senthooran Rajamanoharan and Tom Lieberum and Nicolas Sonnerat and Arthur Conmy and Vikrant Varma and János Kramár and Neel Nanda}, + year={2024}, + eprint={2407.14435}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2407.14435}, +} + +@misc{rajamanoharan2025jumping, + title={Jumping Ahead: Improving Reconstruction Fidelity with JumpReLU Sparse Autoencoders}, + author={Senthooran Rajamanoharan and Tom Lieberum and Nicolas Sonnerat and Arthur Conmy and Vikrant Varma and Janos Kramar and Neel Nanda}, + year={2025}, + url={https://openreview.net/forum?id=mMPaQzgzAN} +} + +@inproceedings{dunefsky2024transcoders, + title={Transcoders find interpretable {LLM} feature circuits}, + author={Jacob Dunefsky and Philippe Chlenski and Neel Nanda}, + booktitle={The Thirty-eighth Annual Conference on Neural Information Processing Systems}, + year={2024}, + url={https://openreview.net/forum?id=J6zHcScAo0} +} + +@inproceedings{gao2025topksae, + title = {Scaling and evaluating TopK Sparse Autoencoders}, + 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}, + booktitle = {The Thirteenth International Conference on Learning Representations}, + year = {2024}, + url = {https://openreview.net/forum?id=tcsZt9ZNKD} +} + +@misc{lindsey2024crosscoders, + title = {Sparse Crosscoders for Cross-Layer Features and Model Diffing}, + author = {Lindsey, Jack and Templeton, Adly and Marcus, Jonathan and Conerly, Thomas and Batson, Joshua and Olah, Christopher}, + note = {Transformer Circuits Thread, Anthropic. Research update, not peer-reviewed.}, + year = {2024}, + month = {October}, + url = {https://transformer-circuits.pub/2024/crosscoders/index.html} +} + +@misc{valentin2025dbksvdscalablealternatingoptimization, + title={DB-KSVD: Scalable Alternating Optimization for Disentangling High-Dimensional Embedding Spaces}, + author={Romeo Valentin and Sydney M. Katz and Vincent Vanhoucke and Mykel J. Kochenderfer}, + year={2025}, + eprint={2505.18441}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2505.18441}, +} + +@inproceedings{leask2024itda, + title = {Inference-Time Decomposition of Activations (ITDA): A Scalable Approach to Interpreting Large Language Models}, + author = {Leask, Patrick and Nanda, Neel and Al Moubayed, Noura}, + booktitle = {Proceedings of the 41st International Conference on Machine Learning (ICML)}, + year = {2024}, + address = {San Diego, CA, USA}, + note = {Poster presentation, East Exhibition Hall A-B \#E-2410, Wed 16 Jul 11:00 a.m.--1:30 p.m. PDT}, + publisher = {PMLR} +} + +% SAE Feature +@misc{bricken2023simplest, + author = {Trenton Bricken and Joshua Batson and Adly Templeton and Adam Jermyn and Tom Henighan and Chris Olah}, + title = {Features as The Simplest Factorization}, + journal = {Transformer Circuits Thread}, + year = {2023}, + month = {May}, + url = {https://transformer-circuits.pub/2023/may-update/index.html#simple-factorization}, + note = {Part of the May 2023 Circuits Updates by the Anthropic interpretability team} +} + +@misc{bloom2024understandingfeatureslogitlens, + title = {Understanding SAE Features with the Logit Lens}, + author = {Joseph Bloom, Johnny Lin}, + year = {2024}, + url = {https://www.lesswrong.com/posts/qykrYY6rXXM7EEs8Q/understanding-sae-features-with-the-logit-lens}, +} + +@inproceedings{stolfo2025antipodal, + title={Antipodal Pairing and Mechanistic Signals in Dense SAE Latents}, + author={Alessandro Stolfo and Ben Peng Wu and Mrinmaya Sachan}, + booktitle={ICLR 2025 Workshop on Building Trust in Language Models and Applications}, + year={2025}, + url={https://openreview.net/forum?id=Zlx6AlEoB0} +} + +@misc{heap2025sparseautoencodersinterpretrandomly, + title={Sparse Autoencoders Can Interpret Randomly Initialized Transformers}, + author={Thomas Heap and Tim Lawson and Lucy Farnik and Laurence Aitchison}, + year={2025}, + eprint={2501.17727}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2501.17727}, +} + +@inproceedings{balagansky2025mechanistic, + title = {Mechanistic Permutability: Match Features Across Layers}, + author = {Nikita Balagansky and Ian Maksimov and Daniil Gavrilov}, + booktitle = {The Thirteenth International Conference on Learning Representations}, + year = {2025}, + url = {https://openreview.net/forum?id=MDvecs7EvO} +} + +@misc{paulo2025sparseautoencoderstraineddata, + title={Sparse Autoencoders Trained on the Same Data Learn Different Features}, + author={Gonçalo Paulo and Nora Belrose}, + year={2025}, + eprint={2501.16615}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2501.16615}, +} + + +% SAE Steering +@inproceedings{zhao-etal-2025-steering, + title = "Steering Knowledge Selection Behaviours in {LLM}s via {SAE}-Based Representation Engineering", + author = "Zhao, Yu and + Devoto, Alessio and + Hong, Giwon and + Du, Xiaotang and + Gema, Aryo Pradipta and + Wang, Hongru and + He, Xuanli and + Wong, Kam-Fai and + Minervini, Pasquale", + editor = "Chiruzzo, Luis and + Ritter, Alan and + Wang, Lu", + 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)", + month = apr, + year = "2025", + address = "Albuquerque, New Mexico", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2025.naacl-long.264/", + doi = "10.18653/v1/2025.naacl-long.264", + pages = "5117--5136", + ISBN = "979-8-89176-189-6" +} + +@misc{liu2024incontextvectorsmakingcontext, + title={In-context Vectors: Making In Context Learning More Effective and Controllable Through Latent Space Steering}, + author={Sheng Liu and Haotian Ye and Lei Xing and James Zou}, + year={2024}, + eprint={2311.06668}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2311.06668}, +} + +@misc{zhao2025steeringknowledgeselectionbehaviours, + title = {Steering Knowledge Selection Behaviours in LLMs via SAE-Based Representation Engineering}, + 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}, + year = {2025}, + eprint = {2410.15999}, + archiveprefix = {arXiv}, + primaryclass = {cs.CL}, + url = {https://arxiv.org/abs/2410.15999} +} + +@inproceedings{lee2025programming, + title={Programming Refusal with Conditional Activation Steering}, + author={Bruce W. Lee and Inkit Padhi and Karthikeyan Natesan Ramamurthy and Erik Miehling and Pierre Dognin and Manish Nagireddy and Amit Dhurandhar}, + booktitle={The Thirteenth International Conference on Learning Representations}, + year={2025}, + url={https://openreview.net/forum?id=Oi47wc10sm} +} + +@article{karvonen2024sieve, + title={Sieve: SAEs Beat Baselines on a Real-World Task (A Code Generation Case Study)}, + author={Karvonen, Adam and Pai, Dhruv and Wang, Mason and Keigwin, Ben}, + journal={Tilde Research Blog}, + year={2024}, + month={12}, + url={https://www.tilderesearch.com/blog/sieve}, + note={Blog post} +} + + +@inproceedings{obrien2025steering, + title={Steering Language Model Refusal with Sparse Autoencoders}, + 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}, + booktitle={ICML 2025 Workshop on Reliable and Responsible Foundation Models}, + year={2025}, + url={https://openreview.net/forum?id=PMK1jdGQoc} +} + +@misc{ferrao2024autosteer, + title = {AutoSteer: Weight-Preserving Reinforcement Learning for Interpretable Model Control}, + author = {Jeremias Lino Ferrao}, + date = {2024-11-25}, + year = {2024}, + organization = {Apart Research}, + note = {Research submission to the research sprint hosted by Apart}, + howpublished = {https://apartresearch.com} +} + +@misc{durmus2024steering, + 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}, + title = {Evaluating Feature Steering: A Case Study in Mitigating Social Biases}, + year = {2024}, + howpublished = {\url{https://anthropic.com/research/evaluating-feature-steering}}, + note = {Anthropic Research} +} + +@inproceedings{cao2024personalized, + title={Personalized Steering of Large Language Models: Versatile Steering Vectors Through Bi-directional Preference Optimization}, + author={Yuanpu Cao and Tianrong Zhang and Bochuan Cao and Ziyi Yin and Lu Lin and Fenglong Ma and Jinghui Chen}, + booktitle={The Thirty-eighth Annual Conference on Neural Information Processing Systems}, + year={2024}, + url={https://openreview.net/forum?id=7qJFkuZdYo} +} + +@inproceedings{soo2025interpretable, + title={Interpretable Steering of Large Language Models with Feature Guided Activation Additions}, + author={Samuel Soo and Wesley Teng and Chandrasekaran Balaganesh and Tan Guoxian and Ming YAN}, + booktitle={ICLR 2025 Workshop on Building Trust in Language Models and Applications}, + year={2025}, + url={https://openreview.net/forum?id=swRxS7s4rB} +} + +@misc{chalnev2024improvingsteeringvectorstargeting, + title={Improving Steering Vectors by Targeting Sparse Autoencoder Features}, + author={Sviatoslav Chalnev and Matthew Siu and Arthur Conmy}, + year={2024}, + eprint={2411.02193}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2411.02193}, +} + +@misc{arad2025saesgoodsteering, + title={SAEs Are Good for Steering -- If You Select the Right Features}, + author={Dana Arad and Aaron Mueller and Yonatan Belinkov}, + year={2025}, + eprint={2505.20063}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2505.20063}, +} + + +% SAE Unlearning +@inproceedings{zhou2025llm, + title={{LLM} Neurosurgeon: Targeted Knowledge Removal in {LLM}s using Sparse Autoencoders}, + author={Dylan Zhou and Kunal Patil and Yifan Sun and Karthik lakshmanan and Senthooran Rajamanoharan and Arthur Conmy}, + booktitle={ICLR 2025 Workshop on Building Trust in Language Models and Applications}, + year={2025}, + url={https://openreview.net/forum?id=aeQeXlG2Pw} +} + +@inproceedings{muhamed2025saes, + title={{SAE}s Can Improve Unlearning: Dynamic Sparse Autoencoder Guardrails for Precision Unlearning in {LLM}s}, + author={Aashiq Muhamed and Jacopo Bonato and Mona T. Diab and Virginia Smith}, + booktitle={ICML 2025 Workshop on Reliable and Responsible Foundation Models}, + year={2025}, + url={https://openreview.net/forum?id=8gFO7ebDLT} +} + +@inproceedings{cywinski2025saeuron, + title={{SA}eUron: Interpretable Concept Unlearning in Diffusion Models with Sparse Autoencoders}, + author={Bartosz Cywi{\'n}ski and Kamil Deja}, + booktitle={Forty-second International Conference on Machine Learning}, + year={2025}, + url={https://openreview.net/forum?id=6N0GxaKdX9} +} + +@inproceedings{wang2025model, + title={Model Unlearning via Sparse Autoencoder Subspace Guided Projections}, + author={Xu Wang and Zihao Li and Benyou Wang and Yan Hu and Difan Zou}, + booktitle={ICML 2025 Workshop on Machine Unlearning for Generative AI}, + year={2025}, + url={https://openreview.net/forum?id=MIlqM98o9I} +} + + +% SAE Probing +@article{gurnee2023finding, + title={Finding Neurons in a Haystack: Case Studies with Sparse Probing}, + author={Wes Gurnee and Neel Nanda and Matthew Pauly and Katherine Harvey and Dmitrii Troitskii and Dimitris Bertsimas}, + journal={Transactions on Machine Learning Research}, + issn={2835-8856}, + year={2023}, + url={https://openreview.net/forum?id=JYs1R9IMJr}, + note={} +} + +% RL + +@misc{schulman2017proximal, + title = {Proximal Policy Optimization Algorithms}, + author = {Schulman, John and Wolski, Filip and Dhariwal, Prafulla and Radford, Alec and Klimov, Oleg}, + year = {2017}, + eprint = {1707.06347}, + archivePrefix= {arXiv}, + primaryClass = {cs.LG}, + url = {https://arxiv.org/abs/1707.06347}, + howpublished = {\url{https://openai.com/index/openai-baselines-ppo/}} +} + +@article{williams1992simple, + title={Simple statistical gradient-following algorithms for connectionist reinforcement learning}, + author={Williams, Ronald J}, + journal={Machine Learning}, + volume={8}, + number={3-4}, + pages={229--256}, + year={1992}, + publisher={Springer} +} + +@article{mnih2015human, + title={Human-level control through deep reinforcement learning}, + 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}, + journal={Nature}, + volume={518}, + number={7540}, + pages={529--533}, + year={2015}, + publisher={Nature Publishing Group} +} + +@inproceedings{zhong2023a, + title={A Theoretical Analysis of Optimistic Proximal Policy Optimization in Linear Markov Decision Processes}, + author={Han Zhong and Tong Zhang}, + booktitle={Thirty-seventh Conference on Neural Information Processing Systems}, + year={2023}, + url={https://openreview.net/forum?id=1bTG4sJ7tN} +} + +@article{lim2023optimality, + title={Optimality Guarantees for Particle Belief Approximation of POMDPs}, + author={Lim, Michael and Becker, Nathan and Kochenderfer, Mykel J and Tomlin, Claire J and Sunberg, Zachary N}, + journal={Journal of Artificial Intelligence Research}, + volume={77}, + pages={1591--1636}, + year={2023}, + doi={10.1613/jair.1.14491}, + url={https://jair.org/index.php/jair/article/view/14491} +} + +@misc{shao2024deepseekmathpushinglimitsmathematical, + title={DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models}, + 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}, + year={2024}, + eprint={2402.03300}, + archivePrefix={arXiv}, + primaryClass={cs.CL}, + url={https://arxiv.org/abs/2402.03300}, +} + +@misc{deepseekai2025deepseekr1, + title={DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning}, + author={DeepSeek-AI}, + year={2025}, + eprint={2501.12948}, + archivePrefix={arXiv}, + primaryClass={cs.CL}, + url={https://arxiv.org/abs/2501.12948}, +} + +% RL with Residual Stream +@inproceedings{yu-etal-2017-learning, + title = "Learning to Skim Text", + author = "Yu, Adams Wei and + Lee, Hongrae and + Le, Quoc", + editor = "Barzilay, Regina and + Kan, Min-Yen", + booktitle = "Proceedings of the 55th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)", + month = jul, + year = "2017", + address = "Vancouver, Canada", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/P17-1172/", + doi = "10.18653/v1/P17-1172", + pages = "1880--1890", +} + +@article{gui2019long, + title={Long Short-Term Memory with Dynamic Skip Connections}, + volume={33}, + url={https://ojs.aaai.org/index.php/AAAI/article/view/4613}, + DOI={10.1609/aaai.v33i01.33016481}, + number={01}, + journal={Proceedings of the AAAI Conference on Artificial Intelligence}, + author={Gui, Tao and Zhang, Qi and Zhao, Lujun and Lin, Yaosong and Peng, Minlong and Gong, Jingjing and Huang, Xuanjing}, + year={2019}, + month={Jul.}, + pages={6481-6488} +} + +@article{xu2024dynamic, + title = {Dynamic Layer Skipping for Large Language Models on Natural Language Understanding Tasks and Machine Translation Using Reinforcement Learning}, + author = {Xu, Wei and Jin, Xiaodong}, + journal = {Frontiers in Computing and Intelligent Systems}, + year = {2024}, + volume = {9}, + number = {3}, + doi = {10.54097/wy0g8m89}, + note = {Uses dynamic layer skipping with adapters and RL to accelerate inference of LLMs; achieves SOTA performance on NLU and MT tasks} +} + + +% RLVR +@inproceedings{yue2025does, + title={Does Reinforcement Learning Really Incentivize Reasoning Capacity in {LLM}s Beyond the Base Model?}, + author={Yang Yue and Zhiqi Chen and Rui Lu and Andrew Zhao and Zhaokai Wang and Yang Yue and Shiji Song and Gao Huang}, + booktitle={2nd AI for Math Workshop @ ICML 2025}, + year={2025}, + url={https://openreview.net/forum?id=upehLVgq1b} +} + +@misc{wu2025generalizationsftreinforcementlearning, + title={On the Generalization of SFT: A Reinforcement Learning Perspective with Reward Rectification}, + 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}, + year={2025}, + eprint={2508.05629}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2508.05629}, +} + +@article{DBLP:journals/corr/abs-2505-10425, + publtype={informal}, + author={Jingyao Wang and Wenwen Qiang and Zeen Song and Changwen Zheng and Hui Xiong}, + title={Learning to Think: Information-Theoretic Reinforcement Fine-Tuning for LLMs}, + year={2025}, + month={May}, + cdate={1746057600000}, + journal={CoRR}, + volume={abs/2505.10425}, + url={https://doi.org/10.48550/arXiv.2505.10425} +} + +% Token Entropy + +@misc{wang20258020rulehighentropyminority, + title={Beyond the 80/20 Rule: High-Entropy Minority Tokens Drive Effective Reinforcement Learning for LLM Reasoning}, + 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}, + year={2025}, + eprint={2506.01939}, + archivePrefix={arXiv}, + primaryClass={cs.CL}, + url={https://arxiv.org/abs/2506.01939}, +} + +@misc{cheng2025reasoningexplorationentropyperspective, + title={Reasoning with Exploration: An Entropy Perspective on Reinforcement Learning for LLMs}, + author={Daixuan Cheng and Shaohan Huang and Xuekai Zhu and Bo Dai and Wayne Xin Zhao and Zhenliang Zhang and Furu Wei}, + year={2025}, + eprint={2506.14758}, + archivePrefix={arXiv}, + primaryClass={cs.CL}, + url={https://arxiv.org/abs/2506.14758}, +} + +@misc{obeso2025realtimedetectionhallucinatedentities, + title={Real-Time Detection of Hallucinated Entities in Long-Form Generation}, + author={Oscar Obeso and Andy Arditi and Javier Ferrando and Joshua Freeman and Cameron Holmes and Neel Nanda}, + year={2025}, + eprint={2509.03531}, + archivePrefix={arXiv}, + primaryClass={cs.CL}, + url={https://arxiv.org/abs/2509.03531}, +} + +% Model + +@InProceedings{pmlr-v27-baldi12a, + title = {Autoencoders, Unsupervised Learning, and Deep Architectures}, + author = {Baldi, Pierre}, + booktitle = {Proceedings of ICML Workshop on Unsupervised and Transfer Learning}, + pages = {37--49}, + year = {2012}, + editor = {Guyon, Isabelle and Dror, Gideon and Lemaire, Vincent and Taylor, Graham and Silver, Daniel}, + volume = {27}, + series = {Proceedings of Machine Learning Research}, + address = {Bellevue, Washington, USA}, + month = {02 Jul}, + publisher = {PMLR}, + pdf = {http://proceedings.mlr.press/v27/baldi12a/baldi12a.pdf}, + url = {https://proceedings.mlr.press/v27/baldi12a.html}, + 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.} +} + +@misc{grattafiori2024llama3herdmodels, + title={The Llama 3 Herd of Models}, + 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}, + year={2024}, + eprint={2407.21783}, + archivePrefix={arXiv}, + primaryClass={cs.AI}, + url={https://arxiv.org/abs/2407.21783}, +} + +@misc{bloom2024gpt2residualsaes, + title = {Open Source Sparse Autoencoders for all Residual Stream Layers of GPT2 Small}, + author = {Joseph Bloom}, + year = {2024}, + url = {https://www.alignmentforum.org/posts/f9EgfLSurAiqRJySD/open-source-sparse-autoencoders-for-all-residual-stream}, +} + +@misc{gemmateam2024gemma2improvingopen, + title={Gemma 2: Improving Open Language Models at a Practical Size}, + 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}, + year={2024}, + eprint={2408.00118}, + archivePrefix={arXiv}, + primaryClass={cs.CL}, + url={https://arxiv.org/abs/2408.00118}, +} + +@article{radford2019language, + author = {Radford, Alec and Wu, Jeffrey and Child, Rewon and Luan, David and Amodei, Dario and Sutskever, Ilya}, + interhash = {b926ece39c03cdf5499f6540cf63babd}, + intrahash = {33e4b003b64b1060334660fbf6db1f3f}, + journal = {OpenAI}, + keywords = {gpt gpt2 languagemodelling transferlearning transformer}, + note = {Accessed: 2024-11-15}, + timestamp = {2024-11-15T12:44:17.000+0100}, + title = {Language Models are Unsupervised Multitask Learners}, + url = {https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf}, + year = 2019 +} + +% Dataset +@inproceedings{hendrycks2021measuring, + title={Measuring Massive Multitask Language Understanding}, + author={Dan Hendrycks and Collin Burns and Steven Basart and Andy Zou and Mantas Mazeika and Dawn Song and Jacob Steinhardt}, + booktitle={International Conference on Learning Representations}, + year={2021}, + url={https://openreview.net/forum?id=d7KBjmI3GmQ} +} + + + +% Benchmark +@inproceedings{parrish-etal-2022-bbq, + title = "{BBQ}: A hand-built bias benchmark for question answering", + author = "Parrish, Alicia and + Chen, Angelica and + Nangia, Nikita and + Padmakumar, Vishakh and + Phang, Jason and + Thompson, Jana and + Htut, Phu Mon and + Bowman, Samuel", + editor = "Muresan, Smaranda and + Nakov, Preslav and + Villavicencio, Aline", + booktitle = "Findings of the Association for Computational Linguistics: ACL 2022", + month = may, + year = "2022", + address = "Dublin, Ireland", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2022.findings-acl.165/", + doi = "10.18653/v1/2022.findings-acl.165", + pages = "2086--2105" +} + +@misc{wei2024measuringshortformfactualitylarge, + title={Measuring short-form factuality in large language models}, + 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}, + year={2024}, + eprint={2411.04368}, + archivePrefix={arXiv}, + primaryClass={cs.CL}, + url={https://openai.com/index/introducing-simpleqa/}, +} + +@inproceedings{mazeika2024harmbench, + title={HarmBench: A Standardized Evaluation Framework for Automated Red Teaming and Robust Refusal}, + 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}, + booktitle={Forty-first International Conference on Machine Learning}, + year={2024}, + url={https://openreview.net/forum?id=f3TUipYU3U} +} + +@inproceedings{rottger-etal-2024-xstest, + title = "{XST}est: A Test Suite for Identifying Exaggerated Safety Behaviours in Large Language Models", + author = {R{\"o}ttger, Paul and + Kirk, Hannah and + Vidgen, Bertie and + Attanasio, Giuseppe and + Bianchi, Federico and + Hovy, Dirk}, + editor = "Duh, Kevin and + Gomez, Helena and + Bethard, Steven", + booktitle = "Proceedings of the 2024 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies (Volume 1: Long Papers)", + month = jun, + year = "2024", + address = "Mexico City, Mexico", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2024.naacl-long.301/", + doi = "10.18653/v1/2024.naacl-long.301", + pages = "5377--5400" +} + +@inproceedings{li2024the, + title={The {WMDP} Benchmark: Measuring and Reducing Malicious Use with Unlearning}, + 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}, + booktitle={Forty-first International Conference on Machine Learning}, + year={2024}, + url={https://openreview.net/forum?id=xlr6AUDuJz} +} + +@inproceedings{king2024hearts, + title={{HEARTS}: A Holistic Framework for Explainable, Sustainable and Robust Text Stereotype Detection}, + author={Theo King and Zekun Wu and Adriano Koshiyama and Emre Kazim and Philip Colin Treleaven}, + booktitle={Neurips Safe Generative AI Workshop 2024}, + year={2024}, + url={https://openreview.net/forum?id=arh91riKiQ} +} + +@inproceedings{wang2024mmlupro, + title={{MMLU}-Pro: A More Robust and Challenging Multi-Task Language Understanding Benchmark}, + 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}, + booktitle={The Thirty-eight Conference on Neural Information Processing Systems Datasets and Benchmarks Track}, + year={2024}, + url={https://openreview.net/forum?id=y10DM6R2r3} +} + +@misc{cobbe2021trainingverifierssolvemath, + title={Training Verifiers to Solve Math Word Problems}, + 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}, + year={2021}, + eprint={2110.14168}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2110.14168}, +} + +% Brain +@article{Pang2023, + 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}, + title = {Geometric constraints on human brain function}, + journal = {Nature}, + volume = {618}, + number = {7965}, + pages = {566--574}, + year = {2023}, + month = {June}, + doi = {10.1038/s41586-023-06098-1}, + url = {https://doi.org/10.1038/s41586-023-06098-1}, + issn = {1476-4687} +} + +@article{Poo2009, + author = {Cindy Poo and Jeffry S. Isaacson}, + title = {Odor representations in olfactory cortex: "sparse" coding, global inhibition, and oscillations}, + journal = {Neuron}, + volume = {62}, + number = {6}, + pages = {850--861}, + year = {2009}, + month = {June}, + doi = {10.1016/j.neuron.2009.05.022}, + issn = {0896-6273}, + pmid = {19555653}, + pmcid = {PMC2702531}, + url = {https://doi.org/10.1016/j.neuron.2009.05.022}, + note = {Supported by NIH grants: F31 DC009366, R01 DC004682, T32 NS061847} +} + +@book{James2007, + author = {William James}, + title = {The Principles of Psychology, Vol. 1}, + publisher = {Cosimo, Inc.}, + year = {2007} +} + +@article{Coppola2022, + 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}, + title = {The complexity of the stream of consciousness}, + journal = {Communications Biology}, + year = {2022}, + volume = {5}, + number = {1}, + pages = {1173}, + doi = {10.1038/s42003-022-04109-x}, + url = {https://doi.org/10.1038/s42003-022-04109-x}, + issn = {2399-3642} +} + +@article{DelPozo2021, + author = {S. M. Del Pozo and H. Laufs and V. Bonhomme and S. Laureys and P. Balenzuela and E. Tagliazucchi}, + title = {Unconsciousness reconfigures modular brain network dynamics}, + journal = {Chaos}, + year = {2021}, + volume = {31}, + number = {9}, + pages = {093117}, + doi = {10.1063/5.0046047}, + issn = {1054-1500}, + pmid = {34598477} +} + +% AI Alignment +@inproceedings{qi2025safety, + title={Safety Alignment Should be Made More Than Just a Few Tokens Deep}, + author={Xiangyu Qi and Ashwinee Panda and Kaifeng Lyu and Xiao Ma and Subhrajit Roy and Ahmad Beirami and Prateek Mittal and Peter Henderson}, + booktitle={The Thirteenth International Conference on Learning Representations}, + year={2025}, + url={https://openreview.net/forum?id=6Mxhg9PtDE} +} + + +% Emergent Misalignment +@inproceedings{qi2024fine, + author={Xiangyu Qi and Yi Zeng and Tinghao Xie and Pin-Yu Chen and Ruoxi Jia and Prateek Mittal and Peter Henderson}, + title={Fine-tuning Aligned Language Models Compromises Safety, Even When Users Do Not Intend To!}, + year={2024}, + cdate={1704067200000}, + url={https://openreview.net/forum?id=hTEGyKf0dZ}, + booktitle={ICLR} +} + +@inproceedings{betley2025emergent, + title={Emergent Misalignment: Narrow finetuning can produce broadly misaligned {LLM}s}, + 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}, + booktitle={Forty-second International Conference on Machine Learning}, + year={2025}, + url={https://openreview.net/forum?id=aOIJ2gVRWW} +} +@inproceedings{casademunt2025steering, + title={Steering Fine-Tuning Generalization with Targeted Concept Ablation}, + author={Helena Casademunt and Caden Juang and Samuel Marks and Senthooran Rajamanoharan and Neel Nanda}, + booktitle={ICLR 2025 Workshop on Building Trust in Language Models and Applications}, + year={2025}, + url={https://openreview.net/forum?id=2HyKWpAB4i} +} + +@inproceedings{ji-etal-2025-language-models, + title = "Language Models Resist Alignment: Evidence From Data Compression", + author = "Ji, Jiaming and + Wang, Kaile and + Qiu, Tianyi Alex and + Chen, Boyuan and + Zhou, Jiayi and + Li, Changye and + Lou, Hantao and + Dai, Josef and + Liu, Yunhuai and + Yang, Yaodong", + editor = "Che, Wanxiang and + Nabende, Joyce and + Shutova, Ekaterina and + Pilehvar, Mohammad Taher", + booktitle = "Proceedings of the 63rd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)", + month = jul, + year = "2025", + address = "Vienna, Austria", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2025.acl-long.1141/", + doi = "10.18653/v1/2025.acl-long.1141", + pages = "23411--23432", + ISBN = "979-8-89176-251-0" +} + +@misc{guan2025deliberativealignmentreasoningenables, + title={Deliberative Alignment: Reasoning Enables Safer Language Models}, + 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}, + year={2024}, + howpublished={\url{https://openai.com/index/deliberative-alignment/}}, +} + +@inproceedings{rawte-etal-2023-troubling, + title = "The Troubling Emergence of Hallucination in Large Language Models - An Extensive Definition, Quantification, and Prescriptive Remediations", + author = "Rawte, Vipula and + Chakraborty, Swagata and + Pathak, Agnibh and + Sarkar, Anubhav and + Tonmoy, S.M Towhidul Islam and + Chadha, Aman and + Sheth, Amit and + Das, Amitava", + editor = "Bouamor, Houda and + Pino, Juan and + Bali, Kalika", + booktitle = "Proceedings of the 2023 Conference on Empirical Methods in Natural Language Processing", + month = dec, + year = "2023", + address = "Singapore", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2023.emnlp-main.155/", + doi = "10.18653/v1/2023.emnlp-main.155", + pages = "2541--2573" +} + +% Memory +@inproceedings{li-etal-2023-large, + title = "Large Language Models with Controllable Working Memory", + author = "Li, Daliang and + Rawat, Ankit Singh and + Zaheer, Manzil and + Wang, Xin and + Lukasik, Michal and + Veit, Andreas and + Yu, Felix and + Kumar, Sanjiv", + editor = "Rogers, Anna and + Boyd-Graber, Jordan and + Okazaki, Naoaki", + booktitle = "Findings of the Association for Computational Linguistics: ACL 2023", + month = jul, + year = "2023", + address = "Toronto, Canada", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2023.findings-acl.112/", + doi = "10.18653/v1/2023.findings-acl.112", + pages = "1774--1793" +} + +@inproceedings{fountas2025humaninspired, + title={Human-inspired Episodic Memory for Infinite Context {LLM}s}, + author={Zafeirios Fountas and Martin Benfeghoul and Adnan Oomerjee and Fenia Christopoulou and Gerasimos Lampouras and Haitham Bou Ammar and Jun Wang}, + booktitle={The Thirteenth International Conference on Learning Representations}, + year={2025}, + url={https://openreview.net/forum?id=BI2int5SAC} +} + + +@article{Tikochinski2025, + author={Tikochinski, Refael + and Goldstein, Ariel + and Meiri, Yoav + and Hasson, Uri + and Reichart, Roi}, + title={Incremental accumulation of linguistic context in artificial and biological neural networks}, + journal={Nature Communications}, + year={2025}, + month={Jan}, + day={18}, + volume={16}, + number={1}, + pages={803}, + issn={2041-1723}, + doi={10.1038/s41467-025-56162-9}, + url={https://doi.org/10.1038/s41467-025-56162-9} +} + + +% Layer sharing +@inproceedings{elhoushi2024, + 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}, + title={LayerSkip: Enabling Early Exit Inference and Self-Speculative Decoding}, + year={2024}, + pages={12622-12642}, + url={https://doi.org/10.18653/v1/2024.acl-long.681}, + booktitle={Proceedings of the 62nd Annual Meeting of the Association for Computational Linguistics} +} + +@inproceedings{ye-etal-2021-tr, + title = "TR-BERT: Dynamic Token Reduction for Accelerating BERT Inference", + author = "Ye, Deming and + Lin, Yankai and + Huang, Yufei and + Sun, Maosong", + editor = "Toutanova, Kristina and + Rumshisky, Anna and + Zettlemoyer, Luke and + Hakkani-Tur, Dilek and + Beltagy, Iz and + Bethard, Steven and + Cotterell, Ryan and + Chakraborty, Tanmoy and + Zhou, Yichao", + booktitle = "Proceedings of the 2021 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies", + month = jun, + year = "2021", + address = "Online", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2021.naacl-main.463/", + doi = "10.18653/v1/2021.naacl-main.463", + pages = "5798--5809" +} + +@misc{raposo2024mixtureofdepths, + title={Mixture-of-Depths: Dynamically allocating compute in transformer-based language models}, + author={David Raposo and Sam Ritter and Blake Richards and Timothy Lillicrap and Peter Conway Humphreys and Adam Santoro}, + year={2024}, + eprint={2404.02258}, + archivePrefix={arXiv}, + primaryClass={cs.LG}, + url={https://arxiv.org/abs/2404.02258}, +} + +% Additional citations for ICML version +@inproceedings{vig2020causal, + title={Investigating gender bias in language models using causal mediation analysis}, + author={Vig, Jesse and Gehrmann, Sebastian and Belinkov, Yonatan and Qian, Sharon and Nevo, Daniel and Singer, Yaron and Shieber, Stuart}, + booktitle={Advances in Neural Information Processing Systems}, + volume={33}, + pages={12388--12401}, + year={2020} +} + +@inproceedings{meng2022locating, + title={Locating and editing factual associations in GPT}, + author={Meng, Kevin and Bau, David and Andonian, Alex and Belinkov, Yonatan}, + booktitle={Advances in Neural Information Processing Systems}, + volume={35}, + pages={17359--17372}, + year={2022} +} + +@article{team2024gemma, + title={Gemma: Open models based on Gemini research and technology}, + 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}, + journal={arXiv preprint arXiv:2403.08295}, + year={2024} +} + +@article{lieberum2024gemma, + title={Gemma scope: Open sparse autoencoders everywhere all at once on Gemma 2}, + 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}, + journal={arXiv preprint arXiv:2408.05147}, + year={2024} +} + +@article{engels2025decomposing, + title={Decomposing the Dark Matter of Sparse Autoencoders}, + author={Engels, Joshua and Riggs, Logan and Tegmark, Max}, + journal={Transactions on Machine Learning Research}, + year={2025} +} + +@inproceedings{wu2025axbench, + title={{AxBench}: Steering {LLM}s? Even Simple Baselines Outperform Sparse Autoencoders}, + 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.}, + booktitle={ICML}, + year={2025} +} +@inproceedings{wang2023interpretability, +title={Interpretability in the Wild: a Circuit for Indirect Object Identification in {GPT}-2 Small}, +author={Kevin Ro Wang and Alexandre Variengien and Arthur Conmy and Buck Shlegeris and Jacob Steinhardt}, +booktitle={The Eleventh International Conference on Learning Representations }, +year={2023}, +url={https://openreview.net/forum?id=NpsVSN6o4ul} +} \ No newline at end of file diff --git a/app/src/content/embeds/banner.html b/app/src/content/embeds/banner.html new file mode 100644 index 0000000000000000000000000000000000000000..a72a0fe0dc29beca5c0bbc2319b08592a97ed724 --- /dev/null +++ b/app/src/content/embeds/banner.html @@ -0,0 +1,389 @@ +
        + + + + diff --git a/app/src/content/embeds/baseline-comparison.html b/app/src/content/embeds/baseline-comparison.html new file mode 100644 index 0000000000000000000000000000000000000000..486b4116dc4cbb52b45e6449c8a9ad27c1b0db64 --- /dev/null +++ b/app/src/content/embeds/baseline-comparison.html @@ -0,0 +1,201 @@ +
        + + diff --git a/app/src/content/embeds/branch-explorer.html b/app/src/content/embeds/branch-explorer.html new file mode 100644 index 0000000000000000000000000000000000000000..be47662da808026ddddb31d2c88ec9797dd8b9c8 --- /dev/null +++ b/app/src/content/embeds/branch-explorer.html @@ -0,0 +1,805 @@ +
        + + + + diff --git a/app/src/content/embeds/critic-trajectories.html b/app/src/content/embeds/critic-trajectories.html new file mode 100644 index 0000000000000000000000000000000000000000..fd056d37945782ff13bd6948242f977532c46f56 --- /dev/null +++ b/app/src/content/embeds/critic-trajectories.html @@ -0,0 +1,504 @@ + + + + + +Critic Value Trajectories + + + + +
        +
        +
        + + + + \ No newline at end of file diff --git a/app/src/content/embeds/crl-flowchart-animated.html b/app/src/content/embeds/crl-flowchart-animated.html new file mode 100644 index 0000000000000000000000000000000000000000..c520f09cbe5b9784037ed1d5c283a5a8313e47bc --- /dev/null +++ b/app/src/content/embeds/crl-flowchart-animated.html @@ -0,0 +1,637 @@ +
        + + + + diff --git a/app/src/content/embeds/feature-frequency.html b/app/src/content/embeds/feature-frequency.html new file mode 100644 index 0000000000000000000000000000000000000000..07e8af06acbc303edd99393cf70ee894f3c8cd65 --- /dev/null +++ b/app/src/content/embeds/feature-frequency.html @@ -0,0 +1,467 @@ + +
        + + diff --git a/app/src/content/embeds/flowchart-svg.svg b/app/src/content/embeds/flowchart-svg.svg new file mode 100644 index 0000000000000000000000000000000000000000..df08be979961f01ae0a594114e2f7ab651e7741c --- /dev/null +++ b/app/src/content/embeds/flowchart-svg.svg @@ -0,0 +1,474 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/content/embeds/layer-heatmap.html b/app/src/content/embeds/layer-heatmap.html new file mode 100644 index 0000000000000000000000000000000000000000..742eb96357e80572d759828ba9f141680d2c53a7 --- /dev/null +++ b/app/src/content/embeds/layer-heatmap.html @@ -0,0 +1,470 @@ + +
        + + diff --git a/app/src/content/embeds/norm-growth.html b/app/src/content/embeds/norm-growth.html new file mode 100644 index 0000000000000000000000000000000000000000..ada3aa3e4ab6d2c6bd3cba4d52dbd38629c7dcf0 --- /dev/null +++ b/app/src/content/embeds/norm-growth.html @@ -0,0 +1,568 @@ + +
        + + diff --git a/app/src/content/embeds/policy-critic-diagram.html b/app/src/content/embeds/policy-critic-diagram.html new file mode 100644 index 0000000000000000000000000000000000000000..e24512aff083009d4349beee641c5c93d9109dab --- /dev/null +++ b/app/src/content/embeds/policy-critic-diagram.html @@ -0,0 +1,245 @@ + +
        + + diff --git a/app/src/content/embeds/results-table.html b/app/src/content/embeds/results-table.html new file mode 100644 index 0000000000000000000000000000000000000000..d9edbadb47db932b08101a981ded0231d7406e1e --- /dev/null +++ b/app/src/content/embeds/results-table.html @@ -0,0 +1,170 @@ + +
        + + diff --git a/app/src/content/embeds/steering-demo.html b/app/src/content/embeds/steering-demo.html new file mode 100644 index 0000000000000000000000000000000000000000..86b17aaace68ad1b6e2b3fa797e4e24894a031e1 --- /dev/null +++ b/app/src/content/embeds/steering-demo.html @@ -0,0 +1,929 @@ +
        + + + + diff --git a/app/src/content/embeds/token-highlights.html b/app/src/content/embeds/token-highlights.html new file mode 100644 index 0000000000000000000000000000000000000000..577904b0b78f3b39555d440b01daa91717f60c3d --- /dev/null +++ b/app/src/content/embeds/token-highlights.html @@ -0,0 +1,722 @@ + +
        + + diff --git a/app/src/env.d.ts b/app/src/env.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..85641e9778e2c4c3713f15670dd849faaa2212b2 --- /dev/null +++ b/app/src/env.d.ts @@ -0,0 +1,7 @@ +/// +/// +/// + +interface ImportMeta { + readonly env: ImportMetaEnv; +} \ No newline at end of file diff --git a/app/src/pages/index.astro b/app/src/pages/index.astro new file mode 100644 index 0000000000000000000000000000000000000000..55e6017d8870a522ca4ac3afc59d6a422cc4dded --- /dev/null +++ b/app/src/pages/index.astro @@ -0,0 +1,343 @@ +--- +import * as ArticleMod from "../content/article.mdx"; + +import Hero from "../components/Hero.astro"; +import Footer from "../components/Footer.astro"; +import ThemeToggle from "../components/ThemeToggle.astro"; +import Seo from "../components/Seo.astro"; +import TableOfContents from "../components/TableOfContents.astro"; +// Default OG image served from public/ +const ogDefaultUrl = "/thumb.auto.jpg"; +import "katex/dist/katex.min.css"; +import "../styles/global.css"; +const articleFM = (ArticleMod as any).frontmatter ?? {}; +const Article = (ArticleMod as any).default; +// Helper to strip HTML tags +const stripHtml = (text: string) => String(text || "").replace(/<[^>]*>/g, ""); +// docTitle without HTML tags for SEO/meta +const docTitle = stripHtml(articleFM?.title ?? "Untitled article"); +// Allow explicit line breaks in the title via "\n" or YAML newlines +const docTitleHtml = (articleFM?.title ?? "Untitled article") + .replace(/\\n/g, "
        ") + .replace(/\n/g, "
        "); +const subtitle = articleFM?.subtitle ?? ""; +const description = articleFM?.description ?? ""; +// Accept authors as string[] or array of objects { name, url, affiliations? } +const rawAuthors = (articleFM as any)?.authors ?? []; +type Affiliation = { id: number; name: string; url?: string }; +type Author = { name: string; url?: string; affiliationIndices?: number[] }; + +// Normalize affiliations from frontmatter: supports strings or objects { id?, name, url? } +const rawAffils = + (articleFM as any)?.affiliations ?? (articleFM as any)?.affiliation ?? []; +const normalizedAffiliations: Affiliation[] = (() => { + const seen: Map = new Map(); + const list: Affiliation[] = []; + const pushUnique = (name: string, url?: string) => { + const key = `${String(name).trim()}|${url ? String(url).trim() : ""}`; + if (seen.has(key)) return seen.get(key)!; + const id = list.length + 1; + list.push({ + id, + name: String(name).trim(), + url: url ? String(url) : undefined, + }); + seen.set(key, id); + return id; + }; + const input = Array.isArray(rawAffils) + ? rawAffils + : rawAffils + ? [rawAffils] + : []; + for (const a of input) { + if (typeof a === "string") { + pushUnique(a); + } else if (a && typeof a === "object") { + const name = a.name ?? a.label ?? a.text ?? a.affiliation ?? ""; + if (!String(name).trim()) continue; + const url = a.url || a.link; + // Respect provided numeric id for display stability if present and sequential; otherwise reassign + pushUnique(String(name), url ? String(url) : undefined); + } + } + return list; +})(); + +// Helper: ensure an affiliation exists and return its id +const ensureAffiliation = (val: any): number | undefined => { + if (val == null) return undefined; + if (typeof val === "number" && Number.isFinite(val) && val > 0) { + return Math.floor(val); + } + const name = + typeof val === "string" + ? val + : (val?.name ?? val?.label ?? val?.text ?? val?.affiliation); + if (!name || !String(name).trim()) return undefined; + const existing = normalizedAffiliations.find( + (a) => a.name === String(name).trim(), + ); + if (existing) return existing.id; + const id = normalizedAffiliations.length + 1; + normalizedAffiliations.push({ + id, + name: String(name).trim(), + url: val?.url || val?.link, + }); + return id; +}; + +// Normalize authors and map affiliations -> indices (Distill-like) +const normalizedAuthors: Author[] = ( + Array.isArray(rawAuthors) ? rawAuthors : [] +) + .map((a: any) => { + if (typeof a === "string") { + return { name: a } as Author; + } + const name = String(a?.name || "").trim(); + const url = a?.url || a?.link; + let indices: number[] | undefined = undefined; + const raw = a?.affiliations ?? a?.affiliation ?? a?.affils; + if (raw != null) { + const entries = Array.isArray(raw) ? raw : [raw]; + const ids = entries + .map(ensureAffiliation) + .filter((x): x is number => typeof x === "number"); + const unique = Array.from(new Set(ids)).sort((x, y) => x - y); + if (unique.length) indices = unique; + } + return { name, url, affiliationIndices: indices } as Author; + }) + .filter((a: Author) => a.name && a.name.trim().length > 0); +const authorNames: string[] = normalizedAuthors.map((a) => a.name); +const published = articleFM?.published ?? undefined; +const tags = articleFM?.tags ?? []; +// Prefer seoThumbImage from frontmatter if provided +const fmOg = articleFM?.seoThumbImage as string | undefined; +const imageAbs: string = + fmOg && fmOg.startsWith("http") + ? fmOg + : Astro.site + ? new URL(fmOg ?? ogDefaultUrl, Astro.site).toString() + : (fmOg ?? ogDefaultUrl); + +// ---- Build citation text & BibTeX from frontmatter ---- +const rawTitle = articleFM?.title ?? "Untitled article"; +const titleFlat = stripHtml(String(rawTitle)) + .replace(/\\n/g, " ") + .replace(/\n/g, " ") + .replace(/\s+/g, " ") + .trim(); +const extractYear = (val: string | undefined): number | undefined => { + if (!val) return undefined; + const d = new Date(val); + if (!Number.isNaN(d.getTime())) return d.getFullYear(); + const m = String(val).match(/(19|20)\d{2}/); + return m ? Number(m[0]) : undefined; +}; + +const year = extractYear(published); +const citationAuthorsText = authorNames.join(", "); +const citationText = `${citationAuthorsText}${year ? ` (${year})` : ""}. "${titleFlat}".`; + +const authorsBib = authorNames.join(" and "); +const keyAuthor = (authorNames[0] || "article") + .split(/\s+/) + .slice(-1)[0] + .toLowerCase(); +const keyTitle = titleFlat + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_|_$/g, ""); +const bibKey = `${keyAuthor}${year ?? ""}_${keyTitle}`; +const doi = (ArticleMod as any)?.frontmatter?.doi + ? String((ArticleMod as any).frontmatter.doi) + : undefined; +const arxivId = (ArticleMod as any)?.frontmatter?.arxiv + ? String((ArticleMod as any).frontmatter.arxiv) + : undefined; +const bibtex = `@article{${bibKey},\n title={${titleFlat}},\n author={${authorsBib}},\n ${year ? `year={${year}},\n ` : ""}${arxivId ? `journal={arXiv preprint arXiv:${arxivId}},\n eprint={${arxivId}},\n archivePrefix={arXiv},\n primaryClass={cs.CL},\n url={https://arxiv.org/abs/${arxivId}},\n ` : ""}${doi ? `doi={${doi}},\n ` : ""}howpublished={\\url{https://seongland.com/article/crl}}\n}`; +const envCollapse = false; +const tableOfContentAutoCollapse = Boolean( + (articleFM as any)?.tableOfContentAutoCollapse ?? + (articleFM as any)?.tableOfContentsAutoCollapse ?? + envCollapse, +); +// Licence note (HTML allowed) +const licence = + (articleFM as any)?.licence ?? + (articleFM as any)?.license ?? + (articleFM as any)?.licenseNote; +--- + + + + + + + + + + + + + + + + + + +
        + +
        +
        +
        +
        + +
        + + + + + + diff --git a/app/src/styles/_base.css b/app/src/styles/_base.css new file mode 100644 index 0000000000000000000000000000000000000000..7dc6c2c2f9d695bf8061bf04be5686a55237e946 --- /dev/null +++ b/app/src/styles/_base.css @@ -0,0 +1,237 @@ +@import "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,200..900;1,200..900&display=swap"; + +html { + font-size: 16px; + line-height: 1.6; + background-color: var(--page-bg); +} + +.content-grid main { + color: var(--text-color); +} + +.content-grid main p { + margin: 0 0 var(--spacing-3); +} + +.content-grid main h2 { + font-weight: 600; + font-size: clamp(22px, 2.6vw, 32px); + line-height: 1.2; + margin: var(--spacing-10) 0 var(--spacing-5); + padding-bottom: var(--spacing-2); + border-bottom: 1px solid var(--border-color); +} + +.content-grid main h3 { + font-weight: 700; + font-size: clamp(18px, 2.1vw, 22px); + line-height: 1.25; + margin: var(--spacing-8) 0 var(--spacing-4); +} + +.content-grid main h4 { + font-weight: 600; + text-transform: uppercase; + font-size: 14px; + line-height: 1.2; + margin: var(--spacing-8) 0 var(--spacing-4); +} + +.content-grid main a { + color: var(--primary-color); + text-decoration: none; + background: var(--sufrace-bg); + border-bottom: 1px solid color-mix(in srgb, var(--primary-color, #007AFF) 40%, transparent); + ; +} + +/* External links with arrow indicator */ +/* .content-grid main a[href^="http"]:not([href*="localhost"]):not([href*="127.0.0.1"]):not([href*="0.0.0.0"])::after { + content: "↗"; + display: inline-block; + margin-left: 2px; + font-size: 0.5em; + opacity: 0.8; + transition: opacity 0.2s ease, transform 0.2s ease; + transform: translateY(-5px); + font-weight: 600; +} */ + +/* External links with arrow indicator */ +/* .content-grid main a[href^="http"]:not([href*="localhost"]):not([href*="127.0.0.1"]):not([href*="0.0.0.0"]) { + white-space: nowrap; +} */ + +.content-grid main a:hover { + color: var(--primary-color-hover); + border-bottom: 1px solid color-mix(in srgb, var(--primary-color, #007AFF) 40%, transparent); + ; +} + +/* Do not underline heading links inside the article (not the TOC) */ +.content-grid main h2 a, +.content-grid main h3 a, +.content-grid main h4 a, +.content-grid main h5 a, +.content-grid main h6 a { + color: inherit; + border-bottom: none; + text-decoration: none; +} + +.content-grid main h2 a:hover, +.content-grid main h3 a:hover, +.content-grid main h4 a:hover, +.content-grid main h5 a:hover, +.content-grid main h6 a:hover { + color: inherit; + border-bottom: none; + text-decoration: none; +} + +.content-grid main ul, +.content-grid main ol { + padding-left: 24px; + margin: 0 0 var(--spacing-3); +} + +.content-grid main li { + margin-bottom: var(--spacing-2); +} + +.content-grid main li:last-child { + margin-bottom: 0; +} + +.content-grid main blockquote { + border-left: 2px solid var(--border-color); + padding-left: var(--spacing-4); + font-style: italic; + color: var(--muted-color); + margin: var(--spacing-4) 0; +} + +.content-grid main hr { + border: none; + border-bottom: 1px solid var(--border-color); + margin: var(--spacing-5) 0; +} + +.muted { + color: var(--muted-color); +} + +[data-footnote-ref] { + margin-left: 4px; +} + +.content-grid main mark { + background-color: color-mix(in srgb, var(--primary-color, #007AFF) 10%, transparent); + border: 1px solid color-mix(in srgb, var(--primary-color) 18%, transparent); + color: inherit; + padding: 4px 6px; + border-radius: 4px; + font-weight: 500; + box-decoration-break: clone; + -webkit-box-decoration-break: clone; +} + +.feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; + margin: 46px 0; +} + +.feature-card { + display: flex; + flex-direction: column; + padding: 16px; + border: 1px solid color-mix(in srgb, var(--primary-color) 40%, transparent); + ; + background: color-mix(in srgb, var(--primary-color, #007AFF) 05%, transparent) !important; + border-radius: 8px; + text-decoration: none; + color: inherit; + transition: all 0.2s ease; +} + +.feature-card:hover { + transform: translateY(-2px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.feature-card strong { + font-size: 14px; + font-weight: 600; + color: var(--text-color); + color: var(--primary-color) !important; + margin-bottom: 0px !important; +} + +.feature-card span { + font-size: 12px; + color: var(--muted-color); + color: var(--primary-color) !important; + margin-bottom: 0px !important; + opacity: 1; +} + +/* Block math: equal vertical spacing */ +.content-grid main p:has(> .katex:only-child) { + text-align: center; + margin: var(--spacing-4) 0; +} + +/* Hero feature badges */ +.hero-features { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 8px; + margin: 12px auto 0; + max-width: 800px; +} +.hero-feat-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 6px; + border: 1px solid color-mix(in srgb, var(--primary-color, #6366f1) 25%, transparent); + background: color-mix(in srgb, var(--primary-color, #6366f1) 6%, transparent); + text-decoration: none; + color: inherit; + font-size: 12px; + transition: all 0.15s ease; +} +.hero-feat-badge:hover { + border-color: color-mix(in srgb, var(--primary-color, #6366f1) 50%, transparent); + background: color-mix(in srgb, var(--primary-color, #6366f1) 12%, transparent); + transform: translateY(-1px); +} +.hero-feat-badge .feat-id { + font-family: 'SF Mono', 'Fira Code', monospace; + font-weight: 600; + color: var(--primary-color, #6366f1); +} +.hero-feat-badge .feat-desc { + color: var(--muted-color); +} +@media (max-width: 768px) { + .hero-feat-badge { font-size: 11px; padding: 3px 8px; } +} + +.katex .tag { + background: none; + border: none; + opacity: 0.4; +} +/* KaTeX ships the formula twice: a hidden MathML twin for assistive tech and the + visual layout. Chrome selects both, so copying any equation used to paste it + twice over. Keep the twin out of the selection; screen readers still read it. */ +.katex-mathml { + user-select: none; + -webkit-user-select: none; +} diff --git a/app/src/styles/_layout.css b/app/src/styles/_layout.css new file mode 100644 index 0000000000000000000000000000000000000000..4d677dbd3df0e450da225d7de0a2999dc77756c0 --- /dev/null +++ b/app/src/styles/_layout.css @@ -0,0 +1,179 @@ +/* ============================================================================ */ +/* Layout – 3-column grid (Table of Contents / Article / Aside) */ +/* ============================================================================ */ + +.content-grid { + max-width: 1280px; + margin: 0 auto; + padding: 0 var(--content-padding-x); + margin-top: 40px; + display: grid; + grid-template-columns: 260px minmax(0, 680px) 260px; + gap: 32px; + align-items: start; +} + +.content-grid>main { + max-width: 100%; + margin: 0; + padding: 0; +} + +.content-grid>main>*:first-child { + margin-top: 0; +} + +@media (--bp-content-collapse) { + .content-grid { + overflow: hidden; + display: block; + margin-top: var(--spacing-2); + } + + .content-grid { + grid-template-columns: 1fr; + } + + .table-of-contents { + position: static; + display: none; + } + + .table-of-contents-mobile { + display: block; + } + + .footer-inner { + grid-template-columns: 1fr; + gap: 16px; + } + + .footer-inner>h3 { + grid-column: auto; + margin-top: 16px; + } + + .footer-inner { + display: block; + padding: 40px 16px; + } +} + + + +/* ============================================================================ */ +/* Width helpers – slightly wider than main column, and full-width to viewport */ +/* ---------------------------------------------------------------------------- */ + +.wide, +.full-width { + box-sizing: border-box; + position: relative; + z-index: var(--z-elevated); + background-color: var(--background-color); +} + +.wide { + /* Target up to ~1100px while staying within viewport minus page gutters */ + width: min(1100px, 100vw - var(--content-padding-x) * 2); + margin-left: 50%; + transform: translateX(-50%); + padding: calc(var(--content-padding-x)*2); + border-radius: calc(var(--button-radius)*2); + background-color: var(--page-bg); +} + +.wide>* { + margin-bottom: 0 !important; +} + +.full-width { + /* Span the full viewport width and center relative to viewport */ + width: 100vw; + margin-left: calc(50% - 50vw); + margin-right: calc(50% - 50vw); +} + +@media (--bp-content-collapse) { + + .wide, + .full-width { + width: 100%; + margin-left: 0; + margin-right: 0; + padding: 0; + transform: none; + } +} + +/* ============================================================================ */ +/* Theme toggle placement */ +/* ============================================================================ */ + +#theme-toggle { + position: absolute; + top: var(--spacing-4); + left: var(--spacing-3); + margin: 0; + z-index: var(--z-overlay); +} + +/* ------------------------------------------------------------------------- */ +/* Hero meta bar responsiveness */ +/* Two columns at collapse breakpoint, then one column below small screens */ +/* ------------------------------------------------------------------------- */ +@media (--bp-sm) { + header.meta .meta-container { + display: flex; + flex-wrap: wrap; + row-gap: 12px; + column-gap: 8px; + max-width: 100%; + padding: 0 var(--spacing-4); + } + + header.meta .meta-container .meta-container-cell { + flex: 1 1 calc(50% - 8px); + min-width: 0; + } +} + +@media (--bp-xxs) { + header.meta .meta-container .meta-container-cell { + flex-basis: 100%; + text-align: center; + } + + /* Center ordered list numbers within meta (e.g., affiliations) */ + header.meta .affiliations { + list-style-position: inside; + padding-left: 0; + margin-left: 0; + } + + header.meta .affiliations li { + text-align: center; + } +} + + +/* ------------------------------------------------------------------------- */ +/* D3 neural embed responsiveness */ +/* Stack canvas (left) over network (right) on small screens */ +/* ------------------------------------------------------------------------- */ +@media (--bp-md) { + .d3-neural .panel { + flex-direction: column; + } + + .d3-neural .panel .left { + flex: 0 0 auto; + width: 100%; + } + + .d3-neural .panel .right { + flex: 0 0 auto; + width: 100%; + min-width: 0; + } +} \ No newline at end of file diff --git a/app/src/styles/_print.css b/app/src/styles/_print.css new file mode 100644 index 0000000000000000000000000000000000000000..682c7085362cf897bf50c37f424e705646aa49da --- /dev/null +++ b/app/src/styles/_print.css @@ -0,0 +1,68 @@ + +/* ============================================================================ */ +/* Print styles */ +/* ========================================================================= */ +@media print { + html, body { background: #fff; } + /* Margins handled by Playwright; avoid extra global margins */ + body { margin: 0; } + + /* Keep the banner (hero), hide non-essential UI elements */ + #theme-toggle { display: none !important; } + + /* Links: remove underline */ + .content-grid main a { text-decoration: none; border-bottom: 1px solid rgba(0,0,0,.2); } + + /* Avoid breaks inside complex blocks */ + .content-grid main pre, + .content-grid main blockquote, + .content-grid main table, + .content-grid main figure { break-inside: avoid; page-break-inside: avoid; } + + /* Soft page breaks around main headings */ + .content-grid main h2 { page-break-before: auto; page-break-after: avoid; break-after: avoid-page; } + + /* Small icon labels not needed when printing */ + .code-lang-chip { display: none !important; } + + /* Adjust more contrasty colors for print */ + :root { + --border-color: rgba(0,0,0,.2); + --link-underline: rgba(0,0,0,.3); + --link-underline-hover: rgba(0,0,0,.4); + } + + /* Force single column to reduce widows/orphans and awkward breaks */ + .content-grid { grid-template-columns: 1fr !important; } + .table-of-contents, .right-aside, .table-of-contents-mobile { display: none !important; } + main > nav:first-of-type { display: none !important; } + + /* Avoid page breaks inside complex visual blocks */ + .hero, + .hero-banner, + .d3-banner, + .d3-banner svg, + .html-embed__card, + .html-embed__card, + .js-plotly-plot, + figure, + pre, + table, + blockquote, + .wide, + .full-width { + break-inside: avoid; + page-break-inside: avoid; + } + /* Prefer keeping header+lead together */ + .hero { page-break-after: avoid; } + } + + + @media print { + .meta-container-cell--pdf { + display: none !important; + } + } + + \ No newline at end of file diff --git a/app/src/styles/_reset.css b/app/src/styles/_reset.css new file mode 100644 index 0000000000000000000000000000000000000000..fdcbd8f8b8bd6d9035f1cfd0c7a14491c8ac44a6 --- /dev/null +++ b/app/src/styles/_reset.css @@ -0,0 +1,13 @@ +html { box-sizing: border-box; } +*, *::before, *::after { box-sizing: inherit; } +body { margin: 0; font-family: var(--default-font-family); color: var(--text-color); } +audio { display: block; width: 100%; } + +img, +picture { + max-width: 100%; + height: auto; + display: block; + position: relative; + z-index: var(--z-elevated); +} diff --git a/app/src/styles/_variables.css b/app/src/styles/_variables.css new file mode 100644 index 0000000000000000000000000000000000000000..7c86044b6bad46214e4e5474d521a0dad5e159e2 --- /dev/null +++ b/app/src/styles/_variables.css @@ -0,0 +1,123 @@ +/* ============================================================================ */ +/* Design Tokens */ +/* ============================================================================ */ +:root { + /* Neutrals */ + --neutral-600: rgb(107, 114, 128); + --neutral-400: rgb(185, 185, 185); + --neutral-300: rgb(228, 228, 228); + --neutral-200: rgb(245, 245, 245); + + --default-font-family: Source Sans Pro, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + + /* Brand (OKLCH base + derived states) */ + --primary-base: oklch(0.65 0.13 195); + --primary-color: var(--primary-base); + --primary-color-hover: oklch(from var(--primary-color) calc(l - 0.05) c h); + --primary-color-active: oklch(from var(--primary-color) calc(l - 0.10) c h); + --on-primary: #ffffff; + + /* Semantic colors */ + --danger-color: oklch(0.65 0.19 25); + --success-color: oklch(0.65 0.15 145); + --info-color: oklch(0.65 0.12 230); + + /* Text & Surfaces */ + --page-bg: #fff; + --surface-bg: #f9f9f9; + --text-color: rgba(0, 0, 0, .85); + --transparent-page-contrast: rgba(255, 255, 255, .85); + --muted-color: rgba(0, 0, 0, .6); + --border-color: rgba(0, 0, 0, .1); + --code-bg: #f6f8fa; + + /* Links */ + --link-underline: var(--primary-color); + --link-underline-hover: var(--primary-color-hover); + + /* Spacing scale */ + --spacing-1: 8px; + --spacing-2: 12px; + --spacing-3: 16px; + --spacing-4: 24px; + --spacing-5: 32px; + --spacing-6: 40px; + --spacing-7: 48px; + --spacing-8: 56px; + --spacing-9: 64px; + --spacing-10: 72px; + + /* Custom Media aliases compiled by PostCSS */ + @custom-media --bp-xxs (max-width: 320px); + @custom-media --bp-xs (max-width: 480px); + @custom-media --bp-sm (max-width: 640px); + @custom-media --bp-md (max-width: 768px); + @custom-media --bp-lg (max-width: 1024px); + @custom-media --bp-xl (max-width: 1280px); + @custom-media --bp-content-collapse (max-width: 1100px); + + /* Layout */ + --content-padding-x: 16px; + /* default page gutter */ + --block-spacing-y: var(--spacing-4); + /* default vertical spacing between block components */ + + /* Config */ + --palette-count: 8; + + /* Button tokens */ + --button-radius: 6px; + --button-padding-x: 16px; + --button-padding-y: 10px; + --button-font-size: 14px; + --button-icon-padding: 8px; + /* Big button */ + --button-big-padding-x: 16px; + --button-big-padding-y: 12px; + --button-big-font-size: 16px; + --button-big-icon-padding: 12px; + + /* Table tokens */ + --table-border-radius: 8px; + --table-header-bg: oklch(from var(--surface-bg) calc(l - 0.02) c h); + --table-row-odd-bg: oklch(from var(--surface-bg) calc(l - 0.01) c h); + + /* Z-index */ + --z-base: 0; + --z-content: 1; + --z-elevated: 10; + --z-overlay: 1000; + --z-modal: 1100; + --z-tooltip: 1200; + + /* Charts (global) */ + --axis-color: var(--muted-color); + --tick-color: var(--text-color); + --grid-color: rgba(0, 0, 0, .08); +} + +/* ============================================================================ */ +/* Dark Theme Overrides */ +/* ============================================================================ */ +[data-theme="dark"] { + --page-bg: #121212; + --surface-bg: #0a0a0a; + --text-color: rgba(255, 255, 255, .9); + --muted-color: rgba(255, 255, 255, .7); + --border-color: rgba(255, 255, 255, .15); + --code-bg: #161616; + --transparent-page-contrast: rgba(0, 0, 0, .85); + + /* Charts (global) */ + --axis-color: var(--muted-color); + --tick-color: var(--muted-color); + --grid-color: rgba(255, 255, 255, .10); + + /* Primary (lower L in dark) */ + --primary-color-hover: oklch(from var(--primary-color) calc(l - 0.05) c h); + --primary-color-active: oklch(from var(--primary-color) calc(l - 0.10) c h); + --on-primary: #121212; + + color-scheme: dark; + background: var(--page-bg); +} \ No newline at end of file diff --git a/app/src/styles/components/_button.css b/app/src/styles/components/_button.css new file mode 100644 index 0000000000000000000000000000000000000000..879af35881a4e36a5c30afbb23907ce62e437aed --- /dev/null +++ b/app/src/styles/components/_button.css @@ -0,0 +1,58 @@ +button, .button { + appearance: none; + background: linear-gradient(15deg, var(--primary-color) 0%, var(--primary-color-hover) 35%); + color: white; + border: 1px solid transparent; + border-radius: var(--button-radius); + padding: var(--button-padding-y) var(--button-padding-x); + font-size: var(--button-font-size); + line-height: 1; + cursor: pointer; + display: inline-block; + text-decoration: none; + transition: background-color .15s ease, border-color .15s ease, box-shadow .15s ease, transform .02s ease; + } + /* Icon-only buttons: equal X/Y padding */ + button:has(> svg:only-child), + .button:has(> svg:only-child) { + padding: var(--button-icon-padding); + } + button:hover, .button:hover { + filter: brightness(96%); + } + button:active, .button:active { + transform: translateY(1px); + } + button:focus-visible, .button:focus-visible { + outline: none; + } + button:disabled, .button:disabled { + opacity: .6; + cursor: not-allowed; + } + + /* Ghost/Muted button: subtle outline, primary color text/border */ + .button--ghost { + background: transparent !important; + color: var(--primary-color) !important; + border-color: var(--primary-color) !important; + } + .button--ghost:hover { + color: var(--primary-color-hover) !important; + border-color: var(--primary-color-hover) !important; + filter: none; + } + + /* Big button: larger padding and font size */ + .button.button--big { + padding: var(--button-big-padding-y) var(--button-big-padding-x); + font-size: var(--button-big-font-size); + } + .button.button--big:has(> svg:only-child) { + padding: var(--button-big-icon-padding); + } + + .button-group .button { + margin: 5px; + } + \ No newline at end of file diff --git a/app/src/styles/components/_card.css b/app/src/styles/components/_card.css new file mode 100644 index 0000000000000000000000000000000000000000..ebdef086994766b8914029525bb8f82dcf1bb1d9 --- /dev/null +++ b/app/src/styles/components/_card.css @@ -0,0 +1,128 @@ +.card { + background: var(--surface-bg); + border: 1px solid var(--border-color) !important; + border-radius: 12px; + padding: var(--spacing-3); + z-index: calc(var(--z-elevated) + 1); + position: relative; + margin-bottom: 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + transition: all 0.2s ease; + overflow: hidden; + text-decoration: none; + color: inherit; + display: block; +} + +.card:hover { + text-decoration: none; + color: inherit; +} + +.card.no-padding, +.card.card--p0 { + padding: 0; +} + +.card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + border-color: var(--muted-color) !important; +} + + +.card figcaption { + color: var(--text-color) !important; + font-weight: 600 !important; + font-size: 1rem !important; + text-align: center !important; + padding: var(--spacing-3) !important; + margin: 0 !important; +} + +.card .ri-root, +.card .ri-root figure { + margin: 0 !important; +} + +.card img { + border-radius: 8px 8px 0 0 !important; + width: 100%; + height: 120px; + object-fit: cover; + border-bottom: 1px solid var(--border-color) !important; + transition: all 0.2s ease; +} + +.card:hover img { + border-color: var(--muted-color) !important; +} + + +.card h3, +.card h4, +.card h5 { + margin-top: 0 !important; + font-weight: 900 !important; + width: 100%; +} + +.card::after { + display: none !important; +} + +.card-title-container { + padding: var(--spacing-3) var(--spacing-4) var(--spacing-4) var(--spacing-4); +} + +.card-title { + color: var(--text-color); + font-size: 0.9rem; + margin: 0 0 var(--spacing-1) 0 !important; + font-weight: 600; + line-height: 1.4; + white-space: normal; +} + +.card-subtitle { + color: var(--muted-color); + font-size: 0.75rem; + margin: 0 !important; + line-height: 1.5; + white-space: normal; +} + +.card-cta { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + justify-content: center; + background: var(--surface-bg); + border: 2px dashed var(--border-color) !important; +} + +.card-cta:hover { + transform: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + border: 2px dashed var(--border-color) !important; +} + +.card-cta-content { + text-align: center; + padding: var(--spacing-4); +} + +.card-cta h3, +.card-cta h4, +.card-cta h5 { + color: var(--text-color); + font-weight: 900 !important; + margin-bottom: var(--spacing-1) !important; +} + +.card-cta p { + color: var(--muted-color); + font-size: 0.9rem; + margin: 0 !important; +} \ No newline at end of file diff --git a/app/src/styles/components/_code.css b/app/src/styles/components/_code.css new file mode 100644 index 0000000000000000000000000000000000000000..04a8b3c5737f18a95e08fbe0711a5eb7b0278235 --- /dev/null +++ b/app/src/styles/components/_code.css @@ -0,0 +1,346 @@ +/* ============================================================================ */ +/* Inline code */ +/* ============================================================================ */ +code { + font-size: 14px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + border-radius: 0.3em; + border: 1px solid var(--border-color); + color: var(--text-color); + font-weight: 400; + line-height: 1.5; +} + +p code, +.note code { + white-space: nowrap; + padding: calc(var(--spacing-1)/3) calc(var(--spacing-1)/2); +} + + +/* ============================================================================ */ +/* Shiki code blocks */ +/* ============================================================================ */ +.astro-code { + position: relative; + border: 1px solid var(--border-color); + background-color: var(--surface-bg) !important; + border-radius: 6px; + padding: 0; + font-size: 14px; + --code-gutter-width: 2.5em; +} + +/* Shared sizing & horizontal scroll for code containers */ +.astro-code, +section.content-grid pre { + width: 100%; + max-width: 100%; + box-sizing: border-box; + -webkit-overflow-scrolling: touch; + padding: 0; + margin-bottom: var(--block-spacing-y) !important; + overflow-x: auto; +} + +section.content-grid pre.astro-code { + margin: 0; + padding: var(--spacing-1) 0; +} + +section.content-grid pre code { + display: inline-block; + min-width: 100%; +} + +/* Wrap long lines only on small screens to prevent layout overflow */ +@media (--bp-content-collapse) { + + .astro-code, + section.content-grid pre { + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; + } + + section.content-grid pre code { + white-space: pre-wrap; + display: block; + min-width: 0; + } +} + +/* Themes */ +[data-theme='light'] .astro-code { + background-color: var(--code-bg); +} + +/* Apply token color from per-span vars exposed by Shiki dual themes */ +[data-theme='light'] .astro-code span { + color: var(--shiki-light) !important; +} + +[data-theme='dark'] .astro-code span { + color: var(--shiki-dark) !important; +} + +/* Optional: boost contrast for light theme */ +[data-theme='light'] .astro-code { + --shiki-foreground: #24292f; + --shiki-background: #ffffff; +} + +/* Line numbers for Shiki-rendered code blocks */ +.astro-code code { + counter-reset: astro-code-line; + display: block; + background: none; + border: none; +} + +.astro-code .line { + display: inline-block; + position: relative; + padding-left: calc(var(--code-gutter-width) + var(--spacing-1)); + min-height: 1.25em; +} + +.astro-code .line::before { + counter-increment: astro-code-line; + content: counter(astro-code-line); + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: calc(var(--code-gutter-width)); + text-align: right; + color: var(--muted-color); + opacity: .3; + user-select: none; + padding-right: var(--spacing-2); + border-right: 1px solid var(--border-color); +} + +.astro-code .line:empty::after { + content: "\00a0"; +} + +/* Hide trailing empty line added by parsers */ +.astro-code code>.line:last-child:empty { + display: none; +} + +/* ============================================================================ */ +/* Non-Shiki pre wrapper (rehype) */ +/* ============================================================================ */ +.code-card { + position: relative; +} + +.code-card .code-copy { + position: absolute; + top: var(--spacing-2); + right: var(--spacing-2); + z-index: 3; + display: none; +} + +.code-card:hover .code-copy { + display: block; +} + +.code-card .code-copy svg { + width: 16px; + height: 16px; + display: block; + fill: currentColor; +} + +.code-card pre { + margin: 0 0 var(--spacing-1); +} + +/* When no copy button (single-line), keep the label in the top-right corner */ +.code-card.no-copy::after { + top: 8px; + right: 8px; +} + +/* ============================================================================ */ +/* Contextual overrides */ +/* ============================================================================ */ +/* Inside Accordions: remove padding and border on code containers */ +.accordion .astro-code { + padding: 0; + border: none; +} + +.accordion .astro-code { + margin-bottom: 0 !important; +} + +.accordion .code-output { + border: none; + border-top: 1px solid var(--border-color) !important; +} + +.accordion pre { + margin-bottom: 0 !important; +} + +.accordion .code-card pre { + margin: 0 !important; +} + +/* ============================================================================ */ +/* Language/extension vignette (bottom-right, discreet) */ +/* ============================================================================ */ +/* .astro-code::after { + content: attr(data-language); + position: absolute; + right: 0; + bottom: 0; + font-size: 10px; + line-height: 1; + text-transform: uppercase; + color: var(--muted-color); + background: var(--surface-bg); + border-top: 1px solid var(--border-color); + border-left: 1px solid var(--border-color); + opacity: 1; + border-radius: 8px 0 0 0; + padding: 4px 6px; + pointer-events: none; +} */ + +/* Fallback if Shiki uses data-lang instead of data-language */ +/* .astro-code[data-lang]::after { content: attr(data-lang); } +.astro-code[data-language="typescript"]::after { content: "ts"; } +.astro-code[data-language="tsx"]::after { content: "tsx"; } +.astro-code[data-language="javascript"]::after, +.astro-code[data-language="node"]::after, +.astro-code[data-language="jsx"]::after { content: "js"; } +.astro-code[data-language="python"]::after { content: "py"; } +.astro-code[data-language="bash"]::after, +.astro-code[data-language="shell"]::after, +.astro-code[data-language="sh"]::after { content: "sh"; } +.astro-code[data-language="markdown"]::after { content: "md"; } +.astro-code[data-language="yaml"]::after, +.astro-code[data-language="yml"]::after { content: "yml"; } +.astro-code[data-language="html"]::after { content: "html"; } +.astro-code[data-language="css"]::after { content: "css"; } +.astro-code[data-language="json"]::after { content: "json"; } */ + +/* In Accordions, keep same bottom-right placement */ +.accordion .astro-code::after { + right: 0; + bottom: 0; +} + +/* ============================================================================ */ +/* Results blocks glued to code blocks */ +/* ============================================================================ */ +.code-output { + position: relative; + background: oklch(from var(--code-bg) calc(l - 0.005) c h); + border: 1px solid var(--border-color); + border-radius: 6px; + margin-top: 0; + margin-bottom: var(--block-spacing-y); + padding: 0 !important; +} + +.code-output pre { + padding: calc(var(--spacing-3) + 6px) var(--spacing-3) var(--spacing-3) var(--spacing-3) !important; +} + +/* If immediately following a code container, keep tight visual connection */ +.code-card+.code-output, +.astro-code+.code-output, +section.content-grid pre+.code-output { + margin-top: 0; + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + box-shadow: inset 0 8px 12px -12px rgba(0, 0, 0, 0.15); +} + +/* Remove bottom margin on code when immediately followed by results */ +.astro-code:has(+ .code-output) { + margin-bottom: 0 !important; +} + +.code-card:has(+ .code-output) .astro-code { + margin-bottom: 0 !important; +} + +section.content-grid pre:has(+ .code-output) { + margin-bottom: 0 !important; +} + +/* Remove bottom border radius on code when followed by results */ +.astro-code:has(+ .code-output) { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.code-card:has(+ .code-output) .astro-code { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +section.content-grid pre:has(+ .code-output) { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +/* Small top-left label */ +.code-output::before { + content: "Output"; + position: absolute; + top: 0; + right: 0; + font-size: 10px; + line-height: 1; + color: var(--muted-color); + text-transform: uppercase; + letter-spacing: 0.04em; + border-top: none; + border-right: none; + border-radius: 0 0 0 6px; + padding: 10px 10px; +} + +/* Very subtle top shadow so results feels under the code */ +.code-output {} + +.code-output> :where(*):first-child { + margin-top: 0 !important; +} + +.code-output> :where(*):last-child { + margin-bottom: 0 !important; +} + +/* ============================================================================ */ +/* Optional filename tag above code blocks */ +/* ============================================================================ */ +.code-filename { + display: inline-block; + font-size: 12px; + line-height: 1; + color: var(--muted-color); + background: var(--surface-bg); + border: 1px solid var(--border-color); + border-bottom: none; + border-radius: 6px 6px 0 0; + padding: 4px 8px; + margin: 0; +} + +.code-filename+.code-card .astro-code, +.code-filename+.astro-code, +.code-filename+section.content-grid pre { + border-top-left-radius: 0; + border-top-right-radius: 6px; +} \ No newline at end of file diff --git a/app/src/styles/components/_form.css b/app/src/styles/components/_form.css new file mode 100644 index 0000000000000000000000000000000000000000..0ad64f3bca63e17cf234d4a3b3e9969bd64a154b --- /dev/null +++ b/app/src/styles/components/_form.css @@ -0,0 +1,246 @@ +/* ============================================================================ */ +/* Form Elements - Modern Minimal Design */ +/* ============================================================================ */ + +/* Select styling with modern chevron */ +select { + background-color: var(--page-bg); + border: 1px solid color-mix(in srgb, var(--primary-color) 50%, var(--border-color)); + border-radius: var(--button-radius); + padding: var(--button-padding-y) calc(var(--button-padding-x)*2) var(--button-padding-y) var(--button-padding-x); + font-family: var(--default-font-family); + font-size: var(--button-font-size); + color: var(--text-color); + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8.825L1.175 4 2.35 2.825 6 6.475 9.65 2.825 10.825 4z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right calc(var(--button-padding-x) + 2px) center; + background-size: 12px; + cursor: pointer; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +select:hover, +select:focus, +select:active { + border-color: var(--primary-color); +} + +select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1); +} + +select:disabled { + opacity: 0.6; + cursor: not-allowed; + background-color: var(--surface-bg); +} + +/* Dark theme select chevron */ +[data-theme="dark"] select { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23bbb' d='M6 8.825L1.175 4 2.35 2.825 6 6.475 9.65 2.825 10.825 4z'/%3E%3C/svg%3E"); +} + +/* Checkbox styling */ +input[type="checkbox"] { + appearance: none; + width: 16px; + height: 16px; + border: 2px solid var(--border-color); + border-radius: 3px; + background-color: var(--page-bg); + cursor: pointer; + position: relative; + transition: all 0.2s ease; + margin-right: var(--spacing-2); +} + +input[type="checkbox"]:hover { + border-color: var(--primary-color); +} + +input[type="checkbox"]:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1); +} + +input[type="checkbox"]:checked { + background-color: var(--primary-color); + border-color: var(--primary-color); +} + +input[type="checkbox"]:checked::before { + content: ''; + position: absolute; + top: 1px; + left: 4px; + width: 4px; + height: 8px; + border: solid var(--on-primary); + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +input[type="checkbox"]:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Radio button styling */ +input[type="radio"] { + appearance: none; + width: 16px; + height: 16px; + border: 2px solid var(--border-color); + border-radius: 50%; + background-color: var(--page-bg); + cursor: pointer; + position: relative; + transition: all 0.2s ease; + margin-right: var(--spacing-2); +} + +input[type="radio"]:hover { + border-color: var(--primary-color); +} + +input[type="radio"]:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1); +} + +input[type="radio"]:checked { + border-color: var(--primary-color); +} + +input[type="radio"]:checked::before { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--primary-color); +} + +input[type="radio"]:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Input text styling for consistency */ +input[type="text"], +input[type="email"], +input[type="password"], +input[type="number"], +input[type="url"], +input[type="search"], +textarea { + appearance: none; + background-color: var(--page-bg); + border: 1px solid var(--border-color); + border-radius: var(--button-radius); + padding: var(--button-padding-y) var(--button-padding-x); + font-family: var(--default-font-family); + font-size: var(--button-font-size); + color: var(--text-color); + transition: border-color 0.2s ease, box-shadow 0.2s ease; + width: 100%; +} + +input[type="text"]:hover, +input[type="email"]:hover, +input[type="password"]:hover, +input[type="number"]:hover, +input[type="url"]:hover, +input[type="search"]:hover, +textarea:hover { + border-color: var(--primary-color); +} + +input[type="text"]:focus, +input[type="email"]:focus, +input[type="password"]:focus, +input[type="number"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1); +} + +input[type="text"]:disabled, +input[type="email"]:disabled, +input[type="password"]:disabled, +input[type="number"]:disabled, +input[type="url"]:disabled, +input[type="search"]:disabled, +textarea:disabled { + opacity: 0.6; + cursor: not-allowed; + background-color: var(--surface-bg); +} + +/* Label styling */ +label { + display: flex; + align-items: center; + font-size: var(--button-font-size); + color: var(--text-color); + cursor: pointer; + margin-bottom: 0; + line-height: 1.4; + user-select: none; +} + +/* Form group spacing */ +.form-group { + margin-bottom: var(--spacing-4); + display: flex; + align-items: center; + gap: var(--spacing-2); +} + +.form-group label { + margin-bottom: 0; +} + +/* Alternative: for vertical form groups */ +.form-group.vertical { + flex-direction: column; + align-items: flex-start; +} + +.form-group.vertical label { + margin-bottom: var(--spacing-1); +} + +/* For inline form elements */ +.form-inline { + display: flex; + align-items: center; + gap: var(--spacing-2); + margin-bottom: var(--spacing-3); +} + +.form-inline label { + margin-bottom: 0; +} + +/* Ensure labels in flex containers don't break alignment */ +div[style*="display: flex"] label, +div[class*="flex"] label, +.trackio-controls label, +.scale-controls label, +.theme-selector label { + margin-bottom: 0 !important; + align-self: center; +} \ No newline at end of file diff --git a/app/src/styles/components/_table.css b/app/src/styles/components/_table.css new file mode 100644 index 0000000000000000000000000000000000000000..952391144740886878f75efee9c0c0aa6243bfab --- /dev/null +++ b/app/src/styles/components/_table.css @@ -0,0 +1,121 @@ +.content-grid main table { + border-collapse: collapse; + table-layout: auto; + margin: 0; + background-color: var(--surface-bg); +} + +.content-grid main th, +.content-grid main td { + border-bottom: 1px solid var(--border-color); + padding: 6px 8px; + font-size: 15px; + white-space: nowrap; + /* prevent squashing; allow horizontal scroll instead */ + word-break: auto-phrase; + /* white-space: break-spaces; */ + vertical-align: top; +} + +.content-grid main thead th { + border-bottom: 1px solid var(--border-color); +} + +.content-grid main thead th { + border-bottom: 1px solid var(--border-color); +} + +.content-grid main thead th { + background: var(--table-header-bg); + padding-top: 10px; + padding-bottom: 10px; + font-weight: 600; +} + +.content-grid main hr { + border: none; + border-bottom: 1px solid var(--border-color); + margin: var(--spacing-5) 0; +} + +/* Scroll wrapper: keeps table 100% width but enables horizontal scroll when needed */ +.content-grid main .table-scroll { + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + border: 1px solid var(--border-color); + border-radius: var(--table-border-radius); + background: var(--surface-bg); + margin: 0 0 var(--block-spacing-y); +} + +.content-grid main .table-scroll>table { + width: fit-content; + min-width: 100%; + max-width: none; +} + +/* Vertical dividers between columns (no outer right border) */ +.content-grid main .table-scroll>table th, +.content-grid main .table-scroll>table td { + border-right: 1px solid var(--border-color); +} + +.content-grid main .table-scroll>table th:last-child, +.content-grid main .table-scroll>table td:last-child { + border-right: none; +} + +.content-grid main .table-scroll>table thead th:first-child { + border-top-left-radius: var(--table-border-radius); +} + +.content-grid main .table-scroll>table thead th:last-child { + border-top-right-radius: var(--table-border-radius); +} + +.content-grid main .table-scroll>table tbody tr:last-child td:first-child { + border-bottom-left-radius: var(--table-border-radius); +} + +.content-grid main .table-scroll>table tbody tr:last-child td:last-child { + border-bottom-right-radius: var(--table-border-radius); +} + +/* Zebra striping for odd rows */ +.content-grid main .table-scroll>table tbody tr:nth-child(odd) td { + background: var(--table-row-odd-bg); +} + +/* Remove bottom border on last row */ +.content-grid main .table-scroll>table tbody tr:last-child td { + border-bottom: none; +} + +/* Accordion context: remove outer borders/radius and fit content flush */ +.accordion .accordion__content .table-scroll { + border: none; + border-radius: 0; + margin: 0; + margin-bottom: 0 !important; +} + +/* Ensure no bottom margin even if table isn't wrapped (fallback) */ +.accordion .accordion__content table { + margin: 0 !important; +} + +.accordion .accordion__content .table-scroll>table thead th:first-child, +.accordion .accordion__content .table-scroll>table thead th:last-child, +.accordion .accordion__content .table-scroll>table tbody tr:last-child td:first-child, +.accordion .accordion__content .table-scroll>table tbody tr:last-child td:last-child { + border-radius: 0; +} + +/* Fallback for browsers without fit-content support */ +@supports not (width: fit-content) { + .content-grid main .table-scroll>table { + width: max-content; + min-width: 100%; + } +} \ No newline at end of file diff --git a/app/src/styles/components/_tag.css b/app/src/styles/components/_tag.css new file mode 100644 index 0000000000000000000000000000000000000000..b12285a34e79044ca9c584b21b0e2ca9f939da96 --- /dev/null +++ b/app/src/styles/components/_tag.css @@ -0,0 +1,14 @@ +.tag-list { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 16px; } + +.tag { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + font-size: 12px; + line-height: 1; + border-radius: var(--button-radius); + background: var(--surface-bg); + border: 1px solid var(--border-color); + color: var(--text-color); +} diff --git a/app/src/styles/crl-theme.css b/app/src/styles/crl-theme.css new file mode 100644 index 0000000000000000000000000000000000000000..962720ac6d1796de6a944e9091b1b5124526c262 --- /dev/null +++ b/app/src/styles/crl-theme.css @@ -0,0 +1,230 @@ +/* ============================================================================ */ +/* CRL Design System - "Rounded" Language */ +/* ============================================================================ */ + +:root { + /* CRL Brand Colors */ + --crl-primary: #4F46E5; + --crl-primary-light: #6366F1; + --crl-primary-dark: #3730A3; + + /* Semantic: Steering Outcomes */ + --crl-correct: #10B981; + --crl-correct-bg: rgba(16, 185, 129, 0.1); + --crl-incorrect: #EF4444; + --crl-incorrect-bg: rgba(239, 68, 68, 0.1); + --crl-corrected: #3B82F6; + --crl-corrected-bg: rgba(59, 130, 246, 0.1); + --crl-misguided: #F59E0B; + --crl-misguided-bg: rgba(245, 158, 11, 0.1); + + /* Task Category Colors */ + --crl-knowledge: #8B5CF6; + --crl-bias: #EC4899; + --crl-safety: #F97316; + --crl-reasoning: #06B6D4; + + /* Rounded Design Tokens */ + --crl-radius-sm: 8px; + --crl-radius-md: 12px; + --crl-radius-lg: 16px; + --crl-radius-xl: 24px; + --crl-radius-pill: 999px; + + /* Glassmorphism */ + --crl-glass-bg: rgba(255, 255, 255, 0.7); + --crl-glass-border: rgba(255, 255, 255, 0.3); + --crl-glass-blur: 12px; +} + +[data-theme="dark"] { + --crl-glass-bg: rgba(18, 18, 18, 0.7); + --crl-glass-border: rgba(255, 255, 255, 0.1); + + --crl-correct-bg: rgba(16, 185, 129, 0.15); + --crl-incorrect-bg: rgba(239, 68, 68, 0.15); + --crl-corrected-bg: rgba(59, 130, 246, 0.15); + --crl-misguided-bg: rgba(245, 158, 11, 0.15); +} + +/* ============================================================================ */ +/* CRL Components */ +/* ============================================================================ */ + +/* Feature Pills */ +.crl-feature-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: var(--crl-radius-pill); + font-size: 13px; + font-weight: 500; + font-family: 'JetBrains Mono', monospace; + background: var(--surface-bg); + border: 1px solid var(--border-color); + transition: all 0.2s ease; +} + +.crl-feature-pill:hover { + border-color: var(--crl-primary); + box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1); +} + +.crl-feature-pill__dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.crl-feature-pill--corrected { border-color: var(--crl-corrected); background: var(--crl-corrected-bg); } +.crl-feature-pill--misguided { border-color: var(--crl-misguided); background: var(--crl-misguided-bg); } +.crl-feature-pill--correct { border-color: var(--crl-correct); background: var(--crl-correct-bg); } +.crl-feature-pill--incorrect { border-color: var(--crl-incorrect); background: var(--crl-incorrect-bg); } + +/* Glass Cards */ +.crl-glass-card { + background: var(--crl-glass-bg); + backdrop-filter: blur(var(--crl-glass-blur)); + -webkit-backdrop-filter: blur(var(--crl-glass-blur)); + border: 1px solid var(--crl-glass-border); + border-radius: var(--crl-radius-lg); + padding: 24px; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.crl-glass-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); +} + +/* Insight Callouts */ +.crl-insight { + position: relative; + padding: 20px 24px; + border-radius: var(--crl-radius-lg); + border-left: 4px solid var(--crl-primary); + background: linear-gradient(135deg, rgba(79, 70, 229, 0.05), rgba(99, 102, 241, 0.02)); + margin: 24px 0; +} + +.crl-insight::before { + content: 'Key Insight'; + display: block; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--crl-primary); + margin-bottom: 8px; +} + +/* Metric Cards */ +.crl-metrics { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 16px; + margin: 24px 0; +} + +.crl-metric { + text-align: center; + padding: 20px 16px; + border-radius: var(--crl-radius-lg); + background: var(--surface-bg); + border: 1px solid var(--border-color); +} + +.crl-metric__value { + font-size: 28px; + font-weight: 700; + color: var(--crl-primary); + line-height: 1.2; +} + +.crl-metric__label { + font-size: 13px; + color: var(--muted-color); + margin-top: 4px; +} + +.crl-metric__gain { + font-size: 14px; + font-weight: 600; + color: var(--crl-correct); + margin-top: 4px; +} + +/* Layer Badge */ +.crl-layer-badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: var(--crl-radius-pill); + font-size: 12px; + font-weight: 600; + font-family: 'JetBrains Mono', monospace; + background: rgba(79, 70, 229, 0.1); + color: var(--crl-primary); +} + +a:has(> .crl-layer-badge) { + text-decoration: none !important; + border-bottom: none !important; + box-shadow: none !important; +} + +a:has(> .crl-layer-badge):hover .crl-layer-badge { + background: rgba(79, 70, 229, 0.2); +} + +/* Comparison Layout */ +.crl-comparison { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin: 24px 0; +} + +@media (max-width: 640px) { + .crl-comparison { + grid-template-columns: 1fr; + } +} + +.crl-comparison__side { + padding: 20px; + border-radius: var(--crl-radius-lg); + border: 1px solid var(--border-color); +} + +.crl-comparison__side--correct { + border-color: var(--crl-correct); + background: var(--crl-correct-bg); +} + +.crl-comparison__side--incorrect { + border-color: var(--crl-incorrect); + background: var(--crl-incorrect-bg); +} + +/* Equation Highlight */ +.crl-equation { + padding: 24px; + border-radius: var(--crl-radius-lg); + background: var(--surface-bg); + border: 1px solid var(--border-color); + margin: 24px 0; + text-align: center; +} + +/* Hero FullWidth fix: remove background that creates "double layer" effect */ +.full-width:has(.html-embed .hero-3d-container) { + background-color: transparent !important; +} +.full-width > .html-embed:has(.hero-3d-container) { + margin-bottom: 0; +} +.full-width > .html-embed:has(.hero-3d-container) .html-embed__card { + border-radius: 0; +} diff --git a/app/src/styles/global.css b/app/src/styles/global.css new file mode 100644 index 0000000000000000000000000000000000000000..d916f443f5474581e4e4dfb7512bca656aae277a --- /dev/null +++ b/app/src/styles/global.css @@ -0,0 +1,12 @@ +@import './_variables.css'; +@import './_reset.css'; +@import './_base.css'; +@import './_layout.css'; +@import './_print.css'; +@import './components/_code.css'; +@import './components/_button.css'; +@import './components/_table.css'; +@import './components/_tag.css'; +@import './components/_card.css'; +@import './components/_form.css'; +@import './crl-theme.css'; \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000000000000000000000000000000000000..eb81ae4a477c7098207164a3e07ffdee752a00fd --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Starting nginx on port 7860..." +exec nginx -g 'daemon off;' diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..2db142ba064df5a44725c7eff4491a525f2897f7 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,58 @@ +worker_processes auto; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + access_log /tmp/access.log; + error_log /tmp/error.log; + + sendfile on; + keepalive_timeout 65; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 5; + gzip_min_length 256; + gzip_types + text/plain + text/css + application/javascript + application/json + application/xml + application/rss+xml + image/svg+xml; + + server { + listen 7860; + server_name localhost; + + root /app/dist; + index index.html; + + # Health check endpoint + location = /health { + return 200 "ok"; + add_header Content-Type text/plain; + } + + # Cache immutable build assets from Astro + location /_astro/ { + expires 30d; + add_header Cache-Control "public, max-age=2592000, immutable"; + } + + location / { + try_files $uri $uri/ /index.html; + } + } +}