v1.2.4: Save/Download buttons on code blocks and messages, file path highlighting
This commit is contained in:
119
www/js/app.js
119
www/js/app.js
@@ -226,6 +226,40 @@
|
||||
return text.replace(/</g, '<').replace(/>/g, '>').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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user