diff --git a/README.md b/README.md
index a09f525..f03ef21 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/android/app/build.gradle b/android/app/build.gradle
index d6c371f..64ef2f2 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -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:!*~'
diff --git a/package.json b/package.json
index 6acfdbf..dd3e531 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/www/css/styles.css b/www/css/styles.css
index 04ee7f3..adcea98 100644
--- a/www/css/styles.css
+++ b/www/css/styles.css
@@ -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;
diff --git a/www/index.html b/www/index.html
index 77296f4..90ef874 100644
--- a/www/index.html
+++ b/www/index.html
@@ -197,13 +197,24 @@
About
-
Z.AI Chat v1.2.3
+
Z.AI Chat v1.2.4
Built with Z.AI SDK & GLM-5.1
Compatible with Android 15/16
Changelog
+ -
+ v1.2.4
+ 2026-05-19
+
+ - Save button on every code block — downloads code as a real file to your phone
+ - Copy & Save buttons on every AI response — one-tap copy or save as .txt
+ - File paths in AI responses are now highlighted and tappable (tap to copy)
+ - Smart filename detection — auto-names saved files from code content
+ - 30+ language-to-extension mappings for proper file saving
+
+
-
v1.2.3
2026-05-19
diff --git a/www/js/app.js b/www/js/app.js
index 673c9b1..0fa0582 100644
--- a/www/js/app.js
+++ b/www/js/app.js
@@ -226,6 +226,40 @@
return text.replace(//g, '>').replace(/\n/g, '
');
}
+ 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 = '' + escapeHtml(lang) + '';
+ var fileName = guessFileName(block.textContent, lang);
+ header.innerHTML = '' + escapeHtml(lang) + '' +
+ '';
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$2$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 =
+ '' +
+ '';
+ 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 =
+ '' +
+ '';
+ div.appendChild(actionBar);
+ }
$('#messages').scrollTop = $('#messages').scrollHeight;
}