From 248872ca84534f3e01f01fac6bd105dd74db0b4e Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 6 Mar 2026 17:17:20 -0800 Subject: [PATCH 01/11] Diff highlighting --- components/blocks/code.js | 23 ++++++++++++++++++++--- lib/languageDisplayNames.js | 10 ++++++++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/components/blocks/code.js b/components/blocks/code.js index 875aec603..47039898f 100644 --- a/components/blocks/code.js +++ b/components/blocks/code.js @@ -7,6 +7,8 @@ import "prismjs/plugins/line-highlight/prism-line-highlight.css"; import "prismjs/plugins/toolbar/prism-toolbar"; import "prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard"; import "prismjs/plugins/normalize-whitespace/prism-normalize-whitespace"; +import "prismjs/plugins/diff-highlight/prism-diff-highlight"; +import "prismjs/plugins/diff-highlight/prism-diff-highlight.css"; import Image from "./image"; @@ -123,7 +125,10 @@ const Code = ({ // Extract language identifier for display const langId = languageClass?.substring(9) || language || "python"; - const displayLanguage = languageDisplayNames[langId] || langId; + const diffMatch = langId.match(/^diff-([\w-]+)$/); + const displayLanguage = diffMatch + ? languageDisplayNames[diffMatch[1]] || diffMatch[1] + : languageDisplayNames[langId] || langId; const showLanguage = langId.toLowerCase() !== "none" && (showAll || !filename); @@ -175,8 +180,20 @@ async function highlightElement( hideCopyButton, ) { if (typeof window !== "undefined") { - // Only import the language if it hasn't been imported before. - if (!languageImports.has(importLanguage)) { + const diffMatch = importLanguage.match(/^diff-([\w-]+)$/); + if (diffMatch) { + for (const lang of ["diff", diffMatch[1]]) { + if (!languageImports.has(lang)) { + try { + await import(`prismjs/components/prism-${lang}`); + languageImports.set(lang, true); + } catch (error) { + console.error(`Prism doesn't support this language: ${lang}`); + } + } + } + languageImports.set(importLanguage, true); + } else if (!languageImports.has(importLanguage)) { try { await import(`prismjs/components/prism-${importLanguage}`); languageImports.set(importLanguage, true); diff --git a/lib/languageDisplayNames.js b/lib/languageDisplayNames.js index b4d87e8df..60893b499 100644 --- a/lib/languageDisplayNames.js +++ b/lib/languageDisplayNames.js @@ -36,6 +36,7 @@ export const languageDisplayNames = { r: "R", docker: "Docker", dockerfile: "Dockerfile", + diff: "Diff", text: "Text", none: "", }; @@ -53,7 +54,12 @@ export const languageToPrism = { }; // Helper to get the Prism-compatible language name -export const getPrismLanguage = (language) => - languageToPrism[language] || language; +export const getPrismLanguage = (language) => { + if (language?.startsWith("diff-")) { + const base = language.substring(5); + return `diff-${languageToPrism[base] || base}`; + } + return languageToPrism[language] || language; +}; export default languageDisplayNames; From 96469293c5d171418393ab56de0e450ce24d9657 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 6 Mar 2026 18:11:52 -0800 Subject: [PATCH 02/11] Smart copy for diffs --- components/blocks/code.js | 60 +++++++++++++++++++++++++++++---- styles/syntax-highlighting.scss | 27 ++++++++++++--- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/components/blocks/code.js b/components/blocks/code.js index 47039898f..8a74111ac 100644 --- a/components/blocks/code.js +++ b/components/blocks/code.js @@ -173,6 +173,52 @@ const Code = ({ ); }; +function getCleanDiffText(textContent) { + return textContent + .split(/\r?\n/) + .filter((line) => !line.startsWith("-")) + .map((line) => + line.startsWith("+") || line.startsWith(" ") ? line.substring(1) : line, + ) + .join("\n"); +} + +function overrideDiffCopyButton(container, codeElement) { + const copyButton = container.querySelector(".copy-to-clipboard-button"); + if (!copyButton) return; + + const timeout = + parseInt(codeElement.getAttribute("data-prismjs-copy-timeout")) || 5000; + + const newButton = copyButton.cloneNode(true); + copyButton.parentNode.replaceChild(newButton, copyButton); + + newButton.addEventListener("click", async () => { + const cleanText = getCleanDiffText(codeElement.textContent); + const span = newButton.querySelector("span"); + + try { + await navigator.clipboard.writeText(cleanText); + if (span) { + span.textContent = "Copied!"; + newButton.setAttribute("data-copy-state", "copy-success"); + } + } catch { + if (span) { + span.textContent = "Press Ctrl+C to copy"; + newButton.setAttribute("data-copy-state", "copy-error"); + } + } + + if (span) { + setTimeout(() => { + span.textContent = "Copy"; + newButton.setAttribute("data-copy-state", "copy"); + }, timeout); + } + }); +} + async function highlightElement( importLanguage, languageImports, @@ -180,9 +226,10 @@ async function highlightElement( hideCopyButton, ) { if (typeof window !== "undefined") { - const diffMatch = importLanguage.match(/^diff-([\w-]+)$/); - if (diffMatch) { - for (const lang of ["diff", diffMatch[1]]) { + const isDiff = importLanguage.startsWith("diff-"); + if (isDiff) { + const baseLang = importLanguage.substring(5); + for (const lang of ["diff", baseLang]) { if (!languageImports.has(lang)) { try { await import(`prismjs/components/prism-${lang}`); @@ -202,16 +249,17 @@ async function highlightElement( } } - // Highlight the code block and conditionally enable toolbar plugins (including copy button) if (codeElement) { - // First highlight the element Prism.highlightElement(codeElement); - // Then activate toolbar plugins on the parent container if copy button is not hidden if (!hideCopyButton) { const container = codeElement.closest(`.${styles.Container}`); if (container) { Prism.highlightAllUnder(container); + + if (isDiff) { + overrideDiffCopyButton(container, codeElement); + } } } } diff --git a/styles/syntax-highlighting.scss b/styles/syntax-highlighting.scss index b78b9ff94..5ff6b4fce 100644 --- a/styles/syntax-highlighting.scss +++ b/styles/syntax-highlighting.scss @@ -284,8 +284,8 @@ code .cdata { font-style: italic !important; } -pre code .deleted, -code .deleted { +pre code .deleted:not(.prefix), +code .deleted:not(.prefix) { color: var(--syntax-deleted) !important; background-color: rgba( 255, @@ -295,8 +295,8 @@ code .deleted { ) !important; /* red-70 with 20% opacity */ } -pre code .inserted, -code .inserted { +pre code .inserted:not(.prefix), +code .inserted:not(.prefix) { color: var(--syntax-inserted) !important; background-color: rgba( 33, @@ -306,6 +306,25 @@ code .inserted { ) !important; /* green-70 with 20% opacity */ } +/* Diff blocks: make all line-level tokens block-level with positioning context */ +code[class*="language-diff-"] > .token.deleted:not(.prefix), +code[class*="language-diff-"] > .token.inserted:not(.prefix), +code[class*="language-diff-"] > .token.unchanged:not(.prefix) { + display: block; + position: relative; +} + +/* Diff prefix gutter: remove prefix from text flow and shift into the left margin */ +code[class*="language-diff-"] .token.prefix { + display: inline-block; + width: 0; + overflow: visible; + position: relative; + left: -1.75ch; + user-select: none; + background-color: transparent !important; +} + /* Table token for inline display */ pre code .table, code .table { From 03206080cb03d2a494bd47249b8458a25e9628a6 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 6 Mar 2026 18:36:20 -0800 Subject: [PATCH 03/11] Hide diff marks --- styles/syntax-highlighting.scss | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/styles/syntax-highlighting.scss b/styles/syntax-highlighting.scss index 5ff6b4fce..6495ecef3 100644 --- a/styles/syntax-highlighting.scss +++ b/styles/syntax-highlighting.scss @@ -314,15 +314,9 @@ code[class*="language-diff-"] > .token.unchanged:not(.prefix) { position: relative; } -/* Diff prefix gutter: remove prefix from text flow and shift into the left margin */ +/* Diff prefix: hidden; replaced by fixed markers added via JS */ code[class*="language-diff-"] .token.prefix { - display: inline-block; - width: 0; - overflow: visible; - position: relative; - left: -1.75ch; - user-select: none; - background-color: transparent !important; + display: none; } /* Table token for inline display */ From a82751ef978d85fb970c969c93906a69f2cc6322 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 6 Mar 2026 18:41:39 -0800 Subject: [PATCH 04/11] Fade edges --- components/blocks/code.module.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/components/blocks/code.module.css b/components/blocks/code.module.css index 5188741bf..f9e9b7170 100644 --- a/components/blocks/code.module.css +++ b/components/blocks/code.module.css @@ -24,6 +24,20 @@ /* Keep in sync with components/blocks/autofunction.module.css */ .Pre { @apply p-6 text-white font-medium relative leading-relaxed; + -webkit-mask-image: linear-gradient( + to right, + transparent 0.375rem, + black 1.5rem, + black calc(100% - 1.5rem), + transparent calc(100% - 0.375rem) + ); + mask-image: linear-gradient( + to right, + transparent 0.375rem, + black 1.5rem, + black calc(100% - 1.5rem), + transparent calc(100% - 0.375rem) + ); } /* Keep in sync with components/blocks/autofunction.module.css */ @@ -116,6 +130,8 @@ :global(.refcard) .Container .Pre { @apply p-0 text-gray-80 h-full; + -webkit-mask-image: none; + mask-image: none; html:global(.dark) & { @apply text-white; From 0d40819f15aafa3def609d9972fdf28b87b8c0e3 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 6 Mar 2026 18:58:39 -0800 Subject: [PATCH 05/11] Diff markers --- components/blocks/code.js | 46 +++++++++++++++++++++++++ components/blocks/code.module.css | 57 ++++++++++++++++++++++++------- styles/syntax-highlighting.scss | 19 +++++++++++ 3 files changed, 109 insertions(+), 13 deletions(-) diff --git a/components/blocks/code.js b/components/blocks/code.js index 8a74111ac..099a7e266 100644 --- a/components/blocks/code.js +++ b/components/blocks/code.js @@ -183,6 +183,51 @@ function getCleanDiffText(textContent) { .join("\n"); } +function addDiffMarkers(container, codeElement) { + const pre = codeElement.closest("pre"); + if (!pre) return; + + const lines = codeElement.textContent.split(/\r?\n/); + // Drop trailing empty line from a final newline + if (lines.length > 0 && lines[lines.length - 1] === "") { + lines.pop(); + } + + const markerEl = document.createElement("div"); + markerEl.className = "diff-markers"; + markerEl.setAttribute("aria-hidden", "true"); + + for (const line of lines) { + const span = document.createElement("span"); + if (line.startsWith("+")) { + span.textContent = "+"; + span.className = "diff-marker-insert"; + } else if (line.startsWith("-")) { + span.textContent = "\u2212"; + span.className = "diff-marker-delete"; + } else { + span.textContent = "\u00A0"; + } + markerEl.appendChild(span); + } + + const codeStyle = getComputedStyle(codeElement); + const topOffset = + pre.offsetTop + parseFloat(getComputedStyle(pre).paddingTop); + + markerEl.style.position = "absolute"; + markerEl.style.top = `${topOffset}px`; + markerEl.style.left = "0"; + markerEl.style.width = "1.5rem"; + markerEl.style.textAlign = "center"; + markerEl.style.fontFamily = codeStyle.fontFamily; + markerEl.style.fontSize = codeStyle.fontSize; + markerEl.style.lineHeight = codeStyle.lineHeight; + markerEl.style.pointerEvents = "none"; + + container.appendChild(markerEl); +} + function overrideDiffCopyButton(container, codeElement) { const copyButton = container.querySelector(".copy-to-clipboard-button"); if (!copyButton) return; @@ -258,6 +303,7 @@ async function highlightElement( Prism.highlightAllUnder(container); if (isDiff) { + addDiffMarkers(container, codeElement); overrideDiffCopyButton(container, codeElement); } } diff --git a/components/blocks/code.module.css b/components/blocks/code.module.css index f9e9b7170..3f9b578de 100644 --- a/components/blocks/code.module.css +++ b/components/blocks/code.module.css @@ -24,19 +24,46 @@ /* Keep in sync with components/blocks/autofunction.module.css */ .Pre { @apply p-6 text-white font-medium relative leading-relaxed; - -webkit-mask-image: linear-gradient( +} + +/* Fade overlays on Container (outside scroll area so they don't scroll) */ +.Container::before, +.Container::after { + content: ""; + position: absolute; + bottom: 0.75rem; + width: 1.5rem; + @apply z-sidebar; + pointer-events: none; +} + +.Container:has(.Header)::before, +.Container:has(.Header)::after { + top: 2.5rem; +} + +.Container:not(:has(.Header))::before, +.Container:not(:has(.Header))::after { + top: 0; +} + +.Container::before { + left: 0; + background: linear-gradient( to right, - transparent 0.375rem, - black 1.5rem, - black calc(100% - 1.5rem), - transparent calc(100% - 0.375rem) + theme("colors.gray.90"), + theme("colors.gray.90") 0.375rem, + transparent ); - mask-image: linear-gradient( - to right, - transparent 0.375rem, - black 1.5rem, - black calc(100% - 1.5rem), - transparent calc(100% - 0.375rem) +} + +.Container::after { + right: 0; + background: linear-gradient( + to left, + theme("colors.gray.90"), + theme("colors.gray.90") 0.375rem, + transparent ); } @@ -61,6 +88,7 @@ @apply absolute top-0 right-0 flex items-center justify-end px-3 h-10 + z-header text-gray-80 text-xs font-medium tracking-wide; } @@ -130,14 +158,17 @@ :global(.refcard) .Container .Pre { @apply p-0 text-gray-80 h-full; - -webkit-mask-image: none; - mask-image: none; html:global(.dark) & { @apply text-white; } } +:global(.refcard) .Container::before, +:global(.refcard) .Container::after { + display: none; +} + :global(.refcard) .Container code { @apply block p-4 h-full; } diff --git a/styles/syntax-highlighting.scss b/styles/syntax-highlighting.scss index 6495ecef3..3a106cb8d 100644 --- a/styles/syntax-highlighting.scss +++ b/styles/syntax-highlighting.scss @@ -319,6 +319,25 @@ code[class*="language-diff-"] .token.prefix { display: none; } +/* Fixed diff markers in the left margin */ +.diff-markers { + display: flex; + flex-direction: column; + z-index: theme("zIndex.header"); +} + +.diff-markers span { + display: block; +} + +.diff-marker-insert { + color: var(--syntax-inserted); +} + +.diff-marker-delete { + color: var(--syntax-deleted); +} + /* Table token for inline display */ pre code .table, code .table { From 50c8023a12eb300f07831f1d5207faa65634d4e5 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 6 Mar 2026 19:08:24 -0800 Subject: [PATCH 06/11] Fix highlight --- styles/syntax-highlighting.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/styles/syntax-highlighting.scss b/styles/syntax-highlighting.scss index 3a106cb8d..f08fd2c93 100644 --- a/styles/syntax-highlighting.scss +++ b/styles/syntax-highlighting.scss @@ -306,6 +306,15 @@ code .inserted:not(.prefix) { ) !important; /* green-70 with 20% opacity */ } +/* Diff blocks: expand code element to content width so block children fill scrollable area */ +code[class*="language-diff-"] { + display: block; + overflow: visible !important; + max-width: none !important; + width: max-content; + min-width: 100%; +} + /* Diff blocks: make all line-level tokens block-level with positioning context */ code[class*="language-diff-"] > .token.deleted:not(.prefix), code[class*="language-diff-"] > .token.inserted:not(.prefix), From d7da23e6cfa0c2ccae51ba4e773541405c7e18e4 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 6 Mar 2026 19:09:35 -0800 Subject: [PATCH 07/11] Cleanup --- styles/syntax-highlighting.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/styles/syntax-highlighting.scss b/styles/syntax-highlighting.scss index f08fd2c93..50f1c529c 100644 --- a/styles/syntax-highlighting.scss +++ b/styles/syntax-highlighting.scss @@ -315,12 +315,11 @@ code[class*="language-diff-"] { min-width: 100%; } -/* Diff blocks: make all line-level tokens block-level with positioning context */ +/* Diff blocks: make all line-level tokens block-level */ code[class*="language-diff-"] > .token.deleted:not(.prefix), code[class*="language-diff-"] > .token.inserted:not(.prefix), code[class*="language-diff-"] > .token.unchanged:not(.prefix) { display: block; - position: relative; } /* Diff prefix: hidden; replaced by fixed markers added via JS */ From 83454295b9020a81238eec3ed06559d47bef446b Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 6 Mar 2026 22:36:01 -0800 Subject: [PATCH 08/11] Add character to every diff line --- components/blocks/code.js | 24 ++++++++++++++++++++---- styles/syntax-highlighting.scss | 2 +- tailwind.config.js | 1 + 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/components/blocks/code.js b/components/blocks/code.js index 099a7e266..5f52f6e39 100644 --- a/components/blocks/code.js +++ b/components/blocks/code.js @@ -6,6 +6,7 @@ import "prismjs/plugins/line-highlight/prism-line-highlight"; import "prismjs/plugins/line-highlight/prism-line-highlight.css"; import "prismjs/plugins/toolbar/prism-toolbar"; import "prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard"; + import "prismjs/plugins/normalize-whitespace/prism-normalize-whitespace"; import "prismjs/plugins/diff-highlight/prism-diff-highlight"; import "prismjs/plugins/diff-highlight/prism-diff-highlight.css"; @@ -177,9 +178,7 @@ function getCleanDiffText(textContent) { return textContent .split(/\r?\n/) .filter((line) => !line.startsWith("-")) - .map((line) => - line.startsWith("+") || line.startsWith(" ") ? line.substring(1) : line, - ) + .map((line) => line.substring(1)) .join("\n"); } @@ -205,7 +204,7 @@ function addDiffMarkers(container, codeElement) { } else if (line.startsWith("-")) { span.textContent = "\u2212"; span.className = "diff-marker-delete"; - } else { + } else if (line.startsWith("=")) { span.textContent = "\u00A0"; } markerEl.appendChild(span); @@ -284,6 +283,23 @@ async function highlightElement( } } } + if (!Prism.languages.diff["unchanged-equal"]) { + Prism.languages.diff["unchanged-equal"] = { + pattern: /^(?:=.*(?:\r\n?|\n|(?![\s\S])))+/m, + alias: ["unchanged"], + inside: { + line: { + pattern: /(.)(?=[\s\S]).*(?:\r\n?|\n)?/, + lookbehind: true, + }, + prefix: { + pattern: /[\s\S]/, + alias: "unchanged", + }, + }, + }; + Prism.languages.diff.PREFIXES["unchanged-equal"] = "="; + } languageImports.set(importLanguage, true); } else if (!languageImports.has(importLanguage)) { try { diff --git a/styles/syntax-highlighting.scss b/styles/syntax-highlighting.scss index 50f1c529c..7fbd8af5f 100644 --- a/styles/syntax-highlighting.scss +++ b/styles/syntax-highlighting.scss @@ -331,7 +331,7 @@ code[class*="language-diff-"] .token.prefix { .diff-markers { display: flex; flex-direction: column; - z-index: theme("zIndex.header"); + z-index: theme("zIndex.overlay_code"); } .diff-markers span { diff --git a/tailwind.config.js b/tailwind.config.js index d9460764b..4ef59ec98 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -187,6 +187,7 @@ module.exports = { base: "0", // Default layer, code line highlights elevated: "10", // Code blocks, search, floating nav, nav icons sidebar: "20", // Sidebar, refcards, lightbox close button + overlay_code: "25", // Diff markers, code block overlays above fades header: "30", // Header, lightbox overlay, chat sticky dropdown: "50", // Version selector, tooltips, dropdown menus overlay: "90", // Full-screen overlays (search modal backdrop) From 6c0c8a341f73116bdf1f33b5e6df30f5c25a122a Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 6 Mar 2026 22:45:33 -0800 Subject: [PATCH 09/11] Update old diff blocks --- .../community-cloud/get-started/quickstart.md | 2 +- .../tutorials/authentication/google.md | 16 ++--- .../tutorials/authentication/microsoft.md | 16 ++--- .../tutorials/llms/chat-response-feedback.md | 72 +++++++++---------- .../tutorials/llms/chat-response-revision.md | 22 +++--- .../installation/cloud-quickstart.md | 2 +- 6 files changed, 65 insertions(+), 65 deletions(-) diff --git a/content/deploy/community-cloud/get-started/quickstart.md b/content/deploy/community-cloud/get-started/quickstart.md index 2307a68b7..747522d81 100644 --- a/content/deploy/community-cloud/get-started/quickstart.md +++ b/content/deploy/community-cloud/get-started/quickstart.md @@ -81,7 +81,7 @@ You will sign in to your GitHub account during this process. Community Cloud wil 1. Go to the app's entrypoint file (`streamlit_app.py`) in the left pane, and change line 3 by adding "Streamlit" inside `st.title`. - ```diff + ```diff-python -st.title("🎈 My new app") +st.title("🎈 My new Streamlit app") ``` diff --git a/content/develop/tutorials/authentication/google.md b/content/develop/tutorials/authentication/google.md index a41fd69ea..bf55e88d1 100644 --- a/content/develop/tutorials/authentication/google.md +++ b/content/develop/tutorials/authentication/google.md @@ -207,10 +207,10 @@ To create an app with user authentication, you'll need to configure your secrets If you don't want to use a callback, you can replace the last line with an equivalent `if` statement: - ```diff - - st.button("Log in with Google", on_click=st.login) - + if st.button("Log in with Google"): - + st.login() + ```diff-python + - st.button("Log in with Google", on_click=st.login) + + if st.button("Log in with Google"): + + st.login() ``` @@ -233,10 +233,10 @@ To create an app with user authentication, you'll need to configure your secrets 1. Replace `st.user` with a personalized greeting: - ```diff - else: - - st.user - + st.header(f"Welcome, {st.user.name}!") + ```diff-python + =else: + - st.user + + st.header(f"Welcome, {st.user.name}!") ``` 1. Add a logout button: diff --git a/content/develop/tutorials/authentication/microsoft.md b/content/develop/tutorials/authentication/microsoft.md index b0d5d4dc7..1880d5231 100644 --- a/content/develop/tutorials/authentication/microsoft.md +++ b/content/develop/tutorials/authentication/microsoft.md @@ -193,10 +193,10 @@ To create an app with user authentication, you'll need to configure your secrets If you don't want to use a callback, you can replace the last line with an equivalent `if` statement: - ```diff - - st.button("Log in with Microsoft", on_click=st.login) - + if st.button("Log in with Microsoft"): - + st.login() + ```diff-python + - st.button("Log in with Microsoft", on_click=st.login) + + if st.button("Log in with Microsoft"): + + st.login() ``` @@ -219,10 +219,10 @@ To create an app with user authentication, you'll need to configure your secrets 1. Replace `st.user` with a personalized greeting: - ```diff - else: - - st.user - + st.header(f"Welcome, {st.user.name}!") + ```diff-python + =else: + - st.user + + st.header(f"Welcome, {st.user.name}!") ``` 1. Add a logout button: diff --git a/content/develop/tutorials/llms/chat-response-feedback.md b/content/develop/tutorials/llms/chat-response-feedback.md index 43af56f36..3529f91e7 100644 --- a/content/develop/tutorials/llms/chat-response-feedback.md +++ b/content/develop/tutorials/llms/chat-response-feedback.md @@ -216,14 +216,14 @@ To make your chat app stateful, you'll save the conversation history into Sessio 1. Add the callback and index argument to your `st.feedback` widget: - ```diff - st.feedback( - "thumbs", - key=f"feedback_{i}", - disabled=feedback is not None, - + on_change=save_feedback, - + args=[i], - ) + ```diff-python + = st.feedback( + = "thumbs", + = key=f"feedback_{i}", + = disabled=feedback is not None, + + on_change=save_feedback, + + args=[i], + = ) ``` When a user interacts with the feedback widget, the callback will update the chat history before the app reruns. @@ -277,36 +277,36 @@ Your app currently allows users to rate any response once. They can submit their If you want users to rate only the _most recent_ response, you can remove the widgets from the chat history: -```diff - for i, message in enumerate(st.session_state.history): - with st.chat_message(message["role"]): - st.write(message["content"]) -- if message["role"] == "assistant": -- feedback = message.get("feedback", None) -- st.session_state[f"feedback_{i}"] = feedback -- st.feedback( -- "thumbs", -- key=f"feedback_{i}", -- disabled=feedback is not None, -- on_change=save_feedback, -- args=[i], -- ) +```diff-python += for i, message in enumerate(st.session_state.history): += with st.chat_message(message["role"]): += st.write(message["content"]) +- if message["role"] == "assistant": +- feedback = message.get("feedback", None) +- st.session_state[f"feedback_{i}"] = feedback +- st.feedback( +- "thumbs", +- key=f"feedback_{i}", +- disabled=feedback is not None, +- on_change=save_feedback, +- args=[i], +- ) ``` Or, if you want to allow users to change their responses, you can just remove the `disabled` parameter: -```diff - for i, message in enumerate(st.session_state.history): - with st.chat_message(message["role"]): - st.write(message["content"]) - if message["role"] == "assistant": - feedback = message.get("feedback", None) - st.session_state[f"feedback_{i}"] = feedback - st.feedback( - "thumbs", - key=f"feedback_{i}", -- disabled=feedback is not None, - on_change=save_feedback, - args=[i], - ) +```diff-python += for i, message in enumerate(st.session_state.history): += with st.chat_message(message["role"]): += st.write(message["content"]) += if message["role"] == "assistant": += feedback = message.get("feedback", None) += st.session_state[f"feedback_{i}"] = feedback += st.feedback( += "thumbs", += key=f"feedback_{i}", +- disabled=feedback is not None, += on_change=save_feedback, += args=[i], += ) ``` diff --git a/content/develop/tutorials/llms/chat-response-revision.md b/content/develop/tutorials/llms/chat-response-revision.md index 3936933ae..f99e729fc 100644 --- a/content/develop/tutorials/llms/chat-response-revision.md +++ b/content/develop/tutorials/llms/chat-response-revision.md @@ -762,17 +762,17 @@ To see another edge case, try this in the running example: When you click a button with an unsubmitted value in another widget, Streamlit will update that widget's value and the button's value in succession before triggering the rerun. Because there isn't a rerun between updating the text area and updating the button, the "**Update**" button doesn't get disabled as expected. To correct this, you can add an extra check for an empty text area within the `"rewrite"` stage: -```diff -- if st.button( -- "Update", type="primary", disabled=new is None or new.strip(". ") == "" -- ): -+ is_empty = new is None or new.strip(". ") == "" -+ if st.button("Update", type="primary", disabled=is_empty) and not is_empty: - st.session_state.history.append({"role": "assistant", "content": new}) - st.session_state.pending = None - st.session_state.validation = {} - st.session_state.stage = "user" - st.rerun() +```diff-python +- if st.button( +- "Update", type="primary", disabled=new is None or new.strip(". ") == "" +- ): ++ is_empty = new is None or new.strip(". ") == "" ++ if st.button("Update", type="primary", disabled=is_empty) and not is_empty: += st.session_state.history.append({"role": "assistant", "content": new}) += st.session_state.pending = None += st.session_state.validation = {} += st.session_state.stage = "user" += st.rerun() ``` Now, if you repeat the listed steps, when the app reruns, the conditional block won't be executed even though the button triggered the rerun. The button will be disabled and the user can proceed as if they had just clicked or tabbed out of the text area. diff --git a/content/get-started/installation/cloud-quickstart.md b/content/get-started/installation/cloud-quickstart.md index 2bbb7c798..634382c4d 100644 --- a/content/get-started/installation/cloud-quickstart.md +++ b/content/get-started/installation/cloud-quickstart.md @@ -81,7 +81,7 @@ If you already created a Community Cloud account and connected GitHub, jump ahea 1. Go to the app's entrypoint file (`streamlit_app.py`) in the left pane, and change line 3 by adding "Streamlit" inside `st.title`. - ```diff + ```diff-python -st.title("🎈 My new app") +st.title("🎈 My new Streamlit app") ``` From f287e1fc342c85ece044a5b4439c8f73f05c3a72 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sun, 8 Mar 2026 19:14:18 -0700 Subject: [PATCH 10/11] Refactor diff styling --- components/blocks/code.js | 33 +++++++++++++------------------ components/blocks/code.module.css | 10 ++++++++++ 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/components/blocks/code.js b/components/blocks/code.js index 5f52f6e39..2de7ebf25 100644 --- a/components/blocks/code.js +++ b/components/blocks/code.js @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef } from "react"; import classNames from "classnames"; import Prism from "prismjs"; import "prismjs/plugins/line-numbers/prism-line-numbers"; @@ -6,12 +6,14 @@ import "prismjs/plugins/line-highlight/prism-line-highlight"; import "prismjs/plugins/line-highlight/prism-line-highlight.css"; import "prismjs/plugins/toolbar/prism-toolbar"; import "prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard"; - import "prismjs/plugins/normalize-whitespace/prism-normalize-whitespace"; import "prismjs/plugins/diff-highlight/prism-diff-highlight"; import "prismjs/plugins/diff-highlight/prism-diff-highlight.css"; import Image from "./image"; +import languageDisplayNames, { + getPrismLanguage, +} from "../../lib/languageDisplayNames"; import styles from "./code.module.css"; @@ -71,10 +73,6 @@ const TryMeButton = ({ code }) => { ); }; -import languageDisplayNames, { - getPrismLanguage, -} from "../../lib/languageDisplayNames"; - // Initialize the cache for imported languages. const languageImports = new Map(); @@ -174,6 +172,7 @@ const Code = ({ ); }; +// Strip deleted lines and diff prefixes (+/=) to produce copy-friendly text. function getCleanDiffText(textContent) { return textContent .split(/\r?\n/) @@ -193,36 +192,29 @@ function addDiffMarkers(container, codeElement) { } const markerEl = document.createElement("div"); - markerEl.className = "diff-markers"; + markerEl.className = `diff-markers ${styles.DiffMarkers}`; markerEl.setAttribute("aria-hidden", "true"); + // Add +/- markers into the left margin of the code block. for (const line of lines) { const span = document.createElement("span"); if (line.startsWith("+")) { span.textContent = "+"; span.className = "diff-marker-insert"; } else if (line.startsWith("-")) { - span.textContent = "\u2212"; + span.textContent = "\u2212"; // minus sign instead of hyphen; visually balanced with + span.className = "diff-marker-delete"; - } else if (line.startsWith("=")) { + } else { + // Non-breaking space keeps this span from collapsing so + // subsequent +/- markers stay vertically aligned with their code lines. span.textContent = "\u00A0"; } markerEl.appendChild(span); } - const codeStyle = getComputedStyle(codeElement); const topOffset = pre.offsetTop + parseFloat(getComputedStyle(pre).paddingTop); - - markerEl.style.position = "absolute"; markerEl.style.top = `${topOffset}px`; - markerEl.style.left = "0"; - markerEl.style.width = "1.5rem"; - markerEl.style.textAlign = "center"; - markerEl.style.fontFamily = codeStyle.fontFamily; - markerEl.style.fontSize = codeStyle.fontSize; - markerEl.style.lineHeight = codeStyle.lineHeight; - markerEl.style.pointerEvents = "none"; container.appendChild(markerEl); } @@ -283,6 +275,9 @@ async function highlightElement( } } } + // Prism's diff grammar only recognizes +/- prefixes. We use "=" for + // unchanged lines to keep code aligned (no visual shift) and to preserve + // leading whitespace that markdown processing would otherwise strip. if (!Prism.languages.diff["unchanged-equal"]) { Prism.languages.diff["unchanged-equal"] = { pattern: /^(?:=.*(?:\r\n?|\n|(?![\s\S])))+/m, diff --git a/components/blocks/code.module.css b/components/blocks/code.module.css index 3f9b578de..ed6ceb3e6 100644 --- a/components/blocks/code.module.css +++ b/components/blocks/code.module.css @@ -26,6 +26,16 @@ @apply p-6 text-white font-medium relative leading-relaxed; } +/* Must match .Pre code typography so markers align line-by-line */ +.DiffMarkers { + position: absolute; + left: 0; + width: 1.5rem; + text-align: center; + pointer-events: none; + @apply font-mono leading-relaxed; +} + /* Fade overlays on Container (outside scroll area so they don't scroll) */ .Container::before, .Container::after { From a3f1c987f3600d9d4cc3620e186f9dad61bd8d8e Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sun, 8 Mar 2026 22:13:18 -0700 Subject: [PATCH 11/11] Move comment --- components/blocks/code.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/blocks/code.js b/components/blocks/code.js index 2de7ebf25..0ea7f0ee1 100644 --- a/components/blocks/code.js +++ b/components/blocks/code.js @@ -181,6 +181,7 @@ function getCleanDiffText(textContent) { .join("\n"); } +// Add +/- markers into the left margin of the code block. function addDiffMarkers(container, codeElement) { const pre = codeElement.closest("pre"); if (!pre) return; @@ -195,7 +196,6 @@ function addDiffMarkers(container, codeElement) { markerEl.className = `diff-markers ${styles.DiffMarkers}`; markerEl.setAttribute("aria-hidden", "true"); - // Add +/- markers into the left margin of the code block. for (const line of lines) { const span = document.createElement("span"); if (line.startsWith("+")) {