diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index bfac60a..4d3fa6c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -24,6 +24,7 @@
"react-markdown": "^10.1.0",
"react-router-dom": "^7.13.0",
"react-syntax-highlighter": "^16.1.0",
+ "react-window": "^1.8.11",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"rehype-slug": "^6.0.0",
@@ -40,6 +41,7 @@
"@types/node": "^24.0.3",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
+ "@types/react-window": "^1.8.8",
"@vitejs/plugin-react": "^4.5.2",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
@@ -118,7 +120,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"dev": true,
- "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -444,7 +445,6 @@
"url": "https://opencollective.com/csstools"
}
],
- "peer": true,
"engines": {
"node": ">=18"
},
@@ -467,7 +467,6 @@
"url": "https://opencollective.com/csstools"
}
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -524,7 +523,6 @@
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -565,7 +563,6 @@
"version": "11.14.1",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1296,7 +1293,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.2.0.tgz",
"integrity": "sha512-NTuyFNen5Z2QY+I242MDZzXnFIVIR6ERxo7vntFi9K1wCgSwvIl0HcAO2OOydKqqKApE6omRiYhpny1ZhGuH7Q==",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/core-downloads-tracker": "^7.2.0",
@@ -1897,7 +1893,8 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -2021,7 +2018,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz",
"integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==",
"dev": true,
- "peer": true,
"dependencies": {
"undici-types": "~7.8.0"
}
@@ -2046,7 +2042,6 @@
"version": "19.1.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
- "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -2056,7 +2051,6 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
"dev": true,
- "peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -2078,6 +2072,16 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-window": {
+ "version": "1.8.8",
+ "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz",
+ "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -2126,7 +2130,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz",
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==",
"dev": true,
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.36.0",
"@typescript-eslint/types": "8.36.0",
@@ -2478,7 +2481,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2525,6 +2527,7 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=8"
}
@@ -2638,7 +2641,6 @@
"url": "https://github.com/sponsors/ai"
}
],
- "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@@ -2995,7 +2997,8 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -3010,7 +3013,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
- "peer": true,
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.0.0"
@@ -3126,7 +3128,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
"dev": true,
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -4065,7 +4066,6 @@
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
- "peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -4271,6 +4271,7 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
+ "peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -4588,6 +4589,12 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/memoize-one": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
+ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
+ "license": "MIT"
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -5471,6 +5478,7 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -5485,6 +5493,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=10"
},
@@ -5496,7 +5505,8 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/prismjs": {
"version": "1.30.0",
@@ -5564,7 +5574,6 @@
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5573,7 +5582,6 @@
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
- "peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -5702,6 +5710,23 @@
"react-dom": ">=16.6.0"
}
},
+ "node_modules/react-window": {
+ "version": "1.8.11",
+ "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz",
+ "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.0.0",
+ "memoize-one": ">=3.1.1 <6"
+ },
+ "engines": {
+ "node": ">8.0.0"
+ },
+ "peerDependencies": {
+ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@@ -6226,7 +6251,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -6367,7 +6391,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6597,7 +6620,6 @@
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -6708,7 +6730,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=12"
},
diff --git a/frontend/package.json b/frontend/package.json
index d547712..161f637 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -28,6 +28,7 @@
"react-markdown": "^10.1.0",
"react-router-dom": "^7.13.0",
"react-syntax-highlighter": "^16.1.0",
+ "react-window": "^1.8.11",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"rehype-slug": "^6.0.0",
@@ -44,6 +45,7 @@
"@types/node": "^24.0.3",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
+ "@types/react-window": "^1.8.8",
"@vitejs/plugin-react": "^4.5.2",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
diff --git a/frontend/src/components/codeblock/CodeBlock.test.tsx b/frontend/src/components/codeblock/CodeBlock.test.tsx
index 0a71d78..777cbe0 100644
--- a/frontend/src/components/codeblock/CodeBlock.test.tsx
+++ b/frontend/src/components/codeblock/CodeBlock.test.tsx
@@ -1,5 +1,5 @@
-import { render, screen, fireEvent, waitFor } from "@testing-library/react";
-import { describe, it, expect, vi } from "vitest";
+import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach } from "vitest";
import CodeBlock from "./CodeBlock";
// Mock clipboard API
@@ -9,6 +9,30 @@ Object.assign(navigator, {
},
});
+// Mock ResizeObserver (jsdom doesn't implement it)
+beforeEach(() => {
+ window.ResizeObserver = vi.fn().mockImplementation((callback) => ({
+ observe: vi.fn((element: Element) => {
+ // Simulate measurement on next microtask
+ Promise.resolve().then(() =>
+ callback(
+ [{ target: element, contentRect: { height: 600, width: 800 } }],
+ {} as ResizeObserver,
+ ),
+ );
+ }),
+ unobserve: vi.fn(),
+ disconnect: vi.fn(),
+ })) as unknown as typeof ResizeObserver;
+});
+
+function generateLongCode(lines: number): string {
+ return Array.from(
+ { length: lines },
+ (_, i) => `x = ${i} # line ${i + 1}`,
+ ).join("\n");
+}
+
describe("CodeBlock", () => {
const sampleCode = `function hello() {
console.log("Hello, world!");
@@ -50,4 +74,60 @@ describe("CodeBlock", () => {
render(
or standard SyntaxHighlighter, not FixedSizeList
+ expect(document.querySelector("pre")).toBeInTheDocument();
+ });
+
+ it("virtualizes code with more than 200 lines", async () => {
+ const longCode = generateLongCode(300);
+ render( );
+
+ // Wait for rAF + ResizeObserver
+ await act(async () => {
+ await new Promise((r) => setTimeout(r, 50));
+ });
+
+ // FixedSizeList renders a container with explicit height
+ await waitFor(() => {
+ const listContainer = document.querySelector(
+ '[style*="height: 600px"]',
+ );
+ expect(listContainer).toBeInTheDocument();
+ });
+ });
+
+ it("does not virtualize when bordered is true", async () => {
+ const longCode = generateLongCode(300);
+ render( );
+
+ await act(async () => {
+ await new Promise((r) => setTimeout(r, 50));
+ });
+
+ // Should not have a FixedSizeList container
+ const listContainer = document.querySelector(
+ '[style*="height: 600px"]',
+ );
+ expect(listContainer).not.toBeInTheDocument();
+ });
+
+ it("copy button works with virtualized code", async () => {
+ const longCode = generateLongCode(300);
+ const writeTextSpy = vi.spyOn(navigator.clipboard, "writeText");
+ render( );
+
+ const copyButton = screen.getByRole("button");
+ fireEvent.click(copyButton);
+
+ expect(writeTextSpy).toHaveBeenCalledWith(longCode);
+ });
+ });
});
diff --git a/frontend/src/components/codeblock/CodeBlock.tsx b/frontend/src/components/codeblock/CodeBlock.tsx
index 16d8755..4c467f7 100644
--- a/frontend/src/components/codeblock/CodeBlock.tsx
+++ b/frontend/src/components/codeblock/CodeBlock.tsx
@@ -1,9 +1,50 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useMemo } from "react";
import { Box, IconButton, Tooltip, useTheme } from "@mui/material";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
+import { FixedSizeList } from "react-window";
+import { useContainerHeight } from "../../lib/hooks/useContainerHeight";
+import type { rendererNode } from "react-syntax-highlighter";
+
+function renderNode(
+ node: rendererNode,
+ stylesheet: { [key: string]: React.CSSProperties },
+ useInlineStyles: boolean,
+ key: React.Key,
+): React.ReactNode {
+ if (node.type === "text") return node.value;
+ if (!node.tagName) return null;
+
+ const classNames = node.properties?.className ?? [];
+ let props: Record;
+ if (useInlineStyles) {
+ const nonToken = classNames.filter((c: string) => c !== "token");
+ const style: React.CSSProperties = {};
+ for (const cls of nonToken) {
+ if (stylesheet[cls]) Object.assign(style, stylesheet[cls]);
+ }
+ // Handle compound selectors (e.g. "keyword.control")
+ if (nonToken.length >= 2) {
+ const a = nonToken.join(".");
+ if (stylesheet[a]) Object.assign(style, stylesheet[a]);
+ const b = nonToken.slice().reverse().join(".");
+ if (stylesheet[b]) Object.assign(style, stylesheet[b]);
+ }
+ props = { key, style };
+ } else {
+ props = { key, className: classNames.join(" ") };
+ }
+
+ const children = node.children?.map((child, i) =>
+ renderNode(child, stylesheet, useInlineStyles, `${key}-${i}`),
+ );
+ return React.createElement(node.tagName, props, children);
+}
+
+const VIRTUALIZATION_LINE_THRESHOLD = 200;
+const LINE_HEIGHT_PX = 20;
interface CodeBlockProps {
code: string;
@@ -33,6 +74,7 @@ export default React.memo(function CodeBlock({ code, bordered = false }: CodeBlo
const [highlighted, setHighlighted] = useState(false);
const theme = useTheme();
const syntaxTheme = theme.palette.mode === "dark" ? oneDark : oneLight;
+ const { containerRef, height: containerHeight } = useContainerHeight();
useEffect(() => {
setHighlighted(false);
@@ -40,6 +82,18 @@ export default React.memo(function CodeBlock({ code, bordered = false }: CodeBlo
return () => cancelAnimationFrame(id);
}, [code]);
+ const lineCount = useMemo(() => {
+ let count = 1;
+ for (let i = 0; i < code.length; i++) {
+ if (code[i] === "\n") count++;
+ }
+ return count;
+ }, [code]);
+
+ const canVirtualize =
+ !bordered && lineCount > VIRTUALIZATION_LINE_THRESHOLD;
+ const shouldVirtualize = canVirtualize && containerHeight > 0;
+
const handleCopy = () => {
navigator.clipboard.writeText(code).then(() => {
setCopied(true);
@@ -47,8 +101,66 @@ export default React.memo(function CodeBlock({ code, bordered = false }: CodeBlo
});
};
+ const virtualizedRenderer = useMemo(() => {
+ if (!shouldVirtualize) return undefined;
+
+ return ({
+ rows,
+ stylesheet,
+ useInlineStyles,
+ }: {
+ rows: rendererNode[];
+ stylesheet: { [key: string]: React.CSSProperties };
+ useInlineStyles: boolean;
+ }): React.ReactNode => (
+
+ {({
+ index,
+ style,
+ }: {
+ index: number;
+ style: React.CSSProperties;
+ }) => (
+
+ {renderNode(
+ rows[index],
+ stylesheet,
+ useInlineStyles,
+ `line-${index}`,
+ )}
+
+ )}
+
+ );
+ }, [shouldVirtualize, containerHeight]);
+
return (
-
+
{/* Copy Button */}
@@ -60,8 +172,11 @@ export default React.memo(function CodeBlock({ code, bordered = false }: CodeBlo
{/* Scrollable Code with Syntax Highlighting */}
- {highlighted ? (
+ {highlighted && (!canVirtualize || shouldVirtualize) ? (
{code}
diff --git a/frontend/src/lib/hooks/useContainerHeight.ts b/frontend/src/lib/hooks/useContainerHeight.ts
new file mode 100644
index 0000000..1f47c79
--- /dev/null
+++ b/frontend/src/lib/hooks/useContainerHeight.ts
@@ -0,0 +1,35 @@
+import { useRef, useState, useCallback, useEffect } from "react";
+
+export function useContainerHeight(): {
+ containerRef: React.RefCallback;
+ height: number;
+} {
+ const [height, setHeight] = useState(0);
+ const observerRef = useRef(null);
+
+ const containerRef = useCallback((node: T | null) => {
+ if (observerRef.current) {
+ observerRef.current.disconnect();
+ observerRef.current = null;
+ }
+
+ if (node) {
+ observerRef.current = new ResizeObserver((entries) => {
+ const entry = entries[0];
+ if (entry) {
+ const newHeight = Math.floor(entry.contentRect.height);
+ setHeight((prev) => (prev !== newHeight ? newHeight : prev));
+ }
+ });
+ observerRef.current.observe(node);
+ }
+ }, []);
+
+ useEffect(() => {
+ return () => {
+ observerRef.current?.disconnect();
+ };
+ }, []);
+
+ return { containerRef, height };
+}
diff --git a/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.tsx b/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.tsx
index cfa550b..77a44a1 100644
--- a/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.tsx
+++ b/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.tsx
@@ -273,8 +273,11 @@ export default function SubmissionCodeSidebar({
{isLoadingCodes ? (