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

@@ -631,6 +631,13 @@ data: [DONE]
## Changelog
### v1.2.4 (2026-05-19)
- **Save button** on every code block — downloads code as a file to your device
- **Copy & Save** buttons on every AI response message
- **File path highlighting** — paths like `/app/build/file.apk` are tappable badges (tap to copy)
- Smart filename detection from code block first line or language
- 30+ language-to-extension mappings for proper file saving
### v1.2.3 (2026-05-19)
- Fixed: Connect button not working — missing `updateSendButton()` function declaration caused JS parse error
- All UI event handlers now correctly initialized on app start

View File

@@ -7,8 +7,8 @@ android {
applicationId "ai.z.chat"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 5
versionName "1.2.3"
versionCode 6
versionName "1.2.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'

View File

@@ -1,6 +1,6 @@
{
"name": "zai-chat",
"version": "1.2.3",
"version": "1.2.4",
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
"main": "index.js",
"scripts": {

View File

@@ -418,7 +418,8 @@ a:hover { text-decoration: underline; }
border-bottom: none;
}
.code-header + pre { border-top-left-radius: 0; border-top-right-radius: 0; }
.copy-btn {
.code-header-actions { display: flex; gap: 4px; align-items: center; }
.copy-btn, .download-btn {
background: var(--accent-dim);
border: none;
color: var(--accent);
@@ -426,8 +427,51 @@ a:hover { text-decoration: underline; }
border-radius: 4px;
font-size: 11px;
cursor: pointer;
transition: all var(--transition);
}
.copy-btn:hover { background: var(--accent); color: white; }
.copy-btn:hover, .download-btn:hover { background: var(--accent); color: white; }
.msg-actions {
display: flex;
gap: 6px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border);
}
.msg-action-btn {
background: var(--accent-dim);
border: none;
color: var(--accent);
padding: 5px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
transition: all var(--transition);
}
.msg-action-btn:hover { background: var(--accent); color: white; }
.msg-action-btn.copied { background: var(--success); color: white; }
.filepath-badge {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--accent-dim);
border: 1px solid var(--accent);
color: var(--accent);
padding: 2px 8px;
border-radius: 4px;
font-family: 'Fira Code', 'JetBrains Mono', monospace;
font-size: 12px;
cursor: pointer;
transition: all var(--transition);
word-break: break-all;
}
.filepath-badge:hover { background: var(--accent); color: white; }
.filepath-badge::before { content: '📁'; font-size: 11px; }
.thinking-indicator {
display: flex;

View File

@@ -197,13 +197,24 @@
</div>
<div class="settings-section">
<h3>About</h3>
<p class="about-text">Z.AI Chat v1.2.3</p>
<p class="about-text">Z.AI Chat v1.2.4</p>
<p class="about-text">Built with Z.AI SDK &amp; GLM-5.1</p>
<p class="about-text">Compatible with Android 15/16</p>
</div>
<div class="settings-section">
<h3>Changelog</h3>
<ul class="changelog-list">
<li>
<span class="changelog-version">v1.2.4</span>
<span class="changelog-date">2026-05-19</span>
<ul>
<li>Save button on every code block — downloads code as a real file to your phone</li>
<li>Copy & Save buttons on every AI response — one-tap copy or save as .txt</li>
<li>File paths in AI responses are now highlighted and tappable (tap to copy)</li>
<li>Smart filename detection — auto-names saved files from code content</li>
<li>30+ language-to-extension mappings for proper file saving</li>
</ul>
</li>
<li>
<span class="changelog-version">v1.2.3</span>
<span class="changelog-date">2026-05-19</span>

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;
}