v1.2.4: Save/Download buttons on code blocks and messages, file path highlighting

This commit is contained in:
admin
2026-05-19 16:15:38 +04:00
Unverified
parent 6b2a68be6a
commit 426787b161
6 changed files with 186 additions and 7 deletions

View File

@@ -226,6 +226,40 @@
return text.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
}
var EXT_MAP = {
python: 'py', py: 'py', javascript: 'js', js: 'js', typescript: 'ts', ts: 'ts',
java: 'java', kotlin: 'kt', kt: 'kt', html: 'html', css: 'css', json: 'json',
xml: 'xml', yaml: 'yml', yml: 'yml', markdown: 'md', md: 'md', sql: 'sql',
shell: 'sh', bash: 'sh', sh: 'sh', powershell: 'ps1', dockerfile: 'Dockerfile',
ruby: 'rb', go: 'go', rust: 'rs', c: 'c', cpp: 'cpp', csharp: 'cs',
swift: 'swift', php: 'php', perl: 'pl', scala: 'scala', groovy: 'groovy',
gradle: 'gradle', properties: 'properties', toml: 'toml', ini: 'ini',
dart: 'dart', lua: 'lua', r: 'r', protobuf: 'proto'
};
function guessFileName(code, lang) {
var firstLine = code.trim().split('\n')[0];
if (/^(\/|\.\/|\.\.\/|[A-Za-z]:\\|[a-zA-Z0-9_\-]+\.[a-zA-Z]{1,10})/.test(firstLine) &&
firstLine.length < 120 && firstLine.split('\n').length === 1 &&
/\.\w+$/.test(firstLine)) {
return firstLine.replace(/^.*[\/\\]/, '');
}
var ext = EXT_MAP[lang] || lang || 'txt';
return 'code.' + ext;
}
function downloadFile(content, filename) {
var blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function() { URL.revokeObjectURL(url); }, 5000);
}
function addCodeHeaders(container) {
container.querySelectorAll('pre code').forEach(function(block) {
var pre = block.parentElement;
@@ -233,7 +267,11 @@
if (!pre.previousElementSibling || !pre.previousElementSibling.classList.contains('code-header')) {
var header = document.createElement('div');
header.className = 'code-header';
header.innerHTML = '<span>' + escapeHtml(lang) + '</span><button class="copy-btn">Copy</button>';
var fileName = guessFileName(block.textContent, lang);
header.innerHTML = '<span>' + escapeHtml(lang) + '</span>' +
'<div class="code-header-actions">' +
'<button class="download-btn" data-filename="' + escapeHtml(fileName) + '">Save</button>' +
'<button class="copy-btn">Copy</button></div>';
pre.parentElement.insertBefore(header, pre);
header.querySelector('.copy-btn').addEventListener('click', function() {
navigator.clipboard.writeText(block.textContent).then(function() {
@@ -241,10 +279,48 @@
setTimeout(function() { this.textContent = 'Copy'; }.bind(this), 2000);
}.bind(this));
});
header.querySelector('.download-btn').addEventListener('click', function() {
var fn = this.getAttribute('data-filename');
var code = block.textContent;
if (fn && /\.\w+$/.test(fn)) {
var lines = code.split('\n');
if (lines[0].trim() === fn || lines[0].trim().endsWith('/' + fn)) {
code = lines.slice(1).join('\n');
}
}
downloadFile(code, fn);
this.textContent = 'Saved!';
setTimeout(function() { this.textContent = 'Save'; }.bind(this), 2000);
});
}
});
}
function highlightFilePaths(html) {
return html.replace(
/(^|[\s(>])(\/(?:[\w\-\.]+\/){1,}[\w\-\.]+\.\w{1,10})([\s)<,]|$)/gm,
'$1<span class="filepath-badge" tabindex="0">$2</span>$3'
);
}
function addFilePathHandlers(container) {
container.querySelectorAll('.filepath-badge').forEach(function(badge) {
badge.addEventListener('click', function() {
var path = this.textContent;
navigator.clipboard.writeText(path).then(function() {
badge.style.background = 'var(--success)';
badge.style.borderColor = 'var(--success)';
badge.style.color = 'white';
setTimeout(function() {
badge.style.background = '';
badge.style.borderColor = '';
badge.style.color = '';
}, 1500);
});
});
});
}
function renderMessages() {
var container = $('#messages');
if (!container) return;
@@ -270,6 +346,33 @@
if (role === 'assistant') {
div.innerHTML = renderMarkdown(content);
addCodeHeaders(div);
var processed = highlightFilePaths(div.innerHTML);
if (processed !== div.innerHTML) {
div.innerHTML = processed;
addFilePathHandlers(div);
}
var actionBar = document.createElement('div');
actionBar.className = 'msg-actions';
actionBar.innerHTML =
'<button class="msg-action-btn msg-copy-btn">Copy</button>' +
'<button class="msg-action-btn msg-save-btn">Save .txt</button>';
div.appendChild(actionBar);
actionBar.querySelector('.msg-copy-btn').addEventListener('click', function() {
var btn = this;
navigator.clipboard.writeText(content).then(function() {
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(function() { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 2000);
});
});
actionBar.querySelector('.msg-save-btn').addEventListener('click', function() {
var btn = this;
var conv = getConversation();
var fn = (conv ? conv.title : 'response').replace(/[^a-zA-Z0-9_\-]/g, '_').substring(0, 40);
downloadFile(content, fn + '.txt');
btn.textContent = 'Saved!';
setTimeout(function() { btn.textContent = 'Save .txt'; }, 2000);
});
} else {
div.textContent = content;
}
@@ -281,6 +384,20 @@
function updateStreamingMessage(div, content) {
div.innerHTML = renderMarkdown(content);
addCodeHeaders(div);
var processed = highlightFilePaths(div.innerHTML);
if (processed !== div.innerHTML) {
div.innerHTML = processed;
addFilePathHandlers(div);
}
var actions = div.querySelector('.msg-actions');
if (!actions) {
var actionBar = document.createElement('div');
actionBar.className = 'msg-actions';
actionBar.innerHTML =
'<button class="msg-action-btn msg-copy-btn">Copy</button>' +
'<button class="msg-action-btn msg-save-btn">Save .txt</button>';
div.appendChild(actionBar);
}
$('#messages').scrollTop = $('#messages').scrollHeight;
}