diff --git a/assets/js/copy-to-llm.js b/assets/js/copy-to-llm.js new file mode 100644 index 00000000000..096e27b5ecb --- /dev/null +++ b/assets/js/copy-to-llm.js @@ -0,0 +1,105 @@ +(function () { + 'use strict'; + + var container = document.getElementById('copy-fulltext'); + if (!container) return; + + var defaultBtn = document.getElementById('copy-fulltext-default'); + var options = container.querySelectorAll('.dropdown-item'); + var toast = document.getElementById('copy-fulltext-toast'); + + // Default button copies markdown (most useful for LLM) + defaultBtn.addEventListener('click', function () { + copyContent('markdown'); + }); + + options.forEach(function (opt) { + opt.addEventListener('click', function () { + copyContent(this.getAttribute('data-copy-type')); + }); + }); + + function getSourceData() { + var mdEl = document.getElementById('copy-fulltext-markdown'); + return { + title: container.getAttribute('data-title') || '', + url: container.getAttribute('data-url') || '', + successMarkdown: container.getAttribute('data-success-markdown') || 'Copied as Markdown', + successText: container.getAttribute('data-success-text') || 'Copied as plain text', + markdown: mdEl ? mdEl.textContent : '' + }; + } + + function getPlainText() { + var contentEl = document.querySelector('.td-content'); + if (!contentEl) return ''; + var clone = contentEl.cloneNode(true); + + // Remove UI elements that shouldn't be copied + var removals = clone.querySelectorAll('.copy-fulltext, .td-page-meta, script, style, .feedback--container'); + removals.forEach(function (el) { el.remove(); }); + + // Replace mermaid SVGs with placeholder (SVG textContent is gibberish) + clone.querySelectorAll('pre.mermaid, .mermaid').forEach(function (el) { + var placeholder = document.createElement('p'); + placeholder.textContent = '[diagram]'; + el.replaceWith(placeholder); + }); + + // Replace images with their alt text + clone.querySelectorAll('img').forEach(function (img) { + var alt = img.getAttribute('alt'); + if (alt) { + var text = document.createElement('span'); + text.textContent = '[image: ' + alt + ']'; + img.replaceWith(text); + } else { + img.remove(); + } + }); + + return clone.textContent.replace(/\n{3,}/g, '\n\n').trim(); + } + + function copyContent(type) { + var data = getSourceData(); + var text = ''; + var actualType = type; + + if (type === 'markdown' && data && data.markdown) { + text = '# ' + data.title + '\n\n' + data.markdown; + if (data.url) { + text += '\n\n---\nSource: ' + data.url; + } + } else { + actualType = 'text'; + text = getPlainText(); + } + + copyToClipboard(text, data, actualType); + } + + function copyToClipboard(text, data, type) { + navigator.clipboard.writeText(text).then(function () { + showFeedback(data, type); + }); + } + + function showFeedback(data, type) { + // Swap icon to checkmark + var icon = defaultBtn.querySelector('i'); + icon.classList.replace('fa-copy', 'fa-check'); + defaultBtn.classList.add('copy-fulltext__btn--success'); + + // Show toast + var toastText = type === 'markdown' ? data.successMarkdown : data.successText; + toast.textContent = '✅ ' + toastText; + toast.classList.add('copy-fulltext__toast--visible'); + + setTimeout(function () { + icon.classList.replace('fa-check', 'fa-copy'); + defaultBtn.classList.remove('copy-fulltext__btn--success'); + toast.classList.remove('copy-fulltext__toast--visible'); + }, 2000); + } +})(); diff --git a/assets/scss/_copy-to-llm.scss b/assets/scss/_copy-to-llm.scss new file mode 100644 index 00000000000..5be3d82e659 --- /dev/null +++ b/assets/scss/_copy-to-llm.scss @@ -0,0 +1,30 @@ +.copy-fulltext { + float: right; + margin-top: 0.25rem; + + &__btn--success { + color: $success !important; + } + + &__toast { + display: none; + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + margin-bottom: 0.5rem; + padding: 0.5rem 0.75rem; + background: $white; + border: 1px solid $gray-300; + border-radius: $border-radius; + box-shadow: 0 4px 12px rgba($black, 0.1); + font-size: 0.875rem; + color: $gray-700; + white-space: nowrap; + z-index: 11; + + &--visible { + display: block; + } + } +} diff --git a/assets/scss/main.scss b/assets/scss/main.scss index 9c024936ce8..ea4a033c3d8 100644 --- a/assets/scss/main.scss +++ b/assets/scss/main.scss @@ -33,6 +33,7 @@ @import "community"; @import "markdown"; @import "safety"; +@import "copy-to-llm"; @if $td-enable-google-fonts { @import url($web-font-path); diff --git a/i18n/en.toml b/i18n/en.toml index 240ac47caf5..a20d16bed68 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -80,3 +80,19 @@ other = "404, Page not found" title = 'Out projects' [taxo.page.header] projects = 'Projects' + +# Copy full text +[copy_full_text] +other = "Copy Full Text" + +[copy_markdown] +other = "Copy Markdown" + +[copy_plain_text] +other = "Copy Text" + +[copy_success_markdown] +other = "Copied as Markdown" + +[copy_success_text] +other = "Copied as plain text" diff --git a/i18n/zh.toml b/i18n/zh.toml index a753e8dd773..e78e2251266 100644 --- a/i18n/zh.toml +++ b/i18n/zh.toml @@ -74,3 +74,19 @@ other = "404, 访问的页面并不存在" title = '项目列表' [taxo.page.header] projects = '项目' + +# Copy full text +[copy_full_text] +other = "复制全文" + +[copy_markdown] +other = "复制 Markdown" + +[copy_plain_text] +other = "复制文本" + +[copy_success_markdown] +other = "已复制 Markdown 格式" + +[copy_success_text] +other = "已复制纯文本" diff --git a/layouts/_default/content.html b/layouts/_default/content.html index cc4c7dd63e9..94cbeb29f97 100644 --- a/layouts/_default/content.html +++ b/layouts/_default/content.html @@ -1,4 +1,5 @@
+ {{ partial "copy-to-llm.html" . }}

{{ .Title }}

{{ with .Params.description }}
{{ . | markdownify }}
{{ end }}
diff --git a/layouts/docs/list.html b/layouts/docs/list.html index 06d69f7119b..24bc589d645 100644 --- a/layouts/docs/list.html +++ b/layouts/docs/list.html @@ -1,5 +1,6 @@ {{ define "main" }}
+ {{ partial "copy-to-llm.html" . }}

{{ .Title }}

{{ with .Params.description }}
{{ . | markdownify }}
{{ end }}