v1.2.4: Save/Download buttons on code blocks and messages, file path highlighting
This commit is contained in:
@@ -631,6 +631,13 @@ data: [DONE]
|
|||||||
|
|
||||||
## Changelog
|
## 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)
|
### v1.2.3 (2026-05-19)
|
||||||
- Fixed: Connect button not working — missing `updateSendButton()` function declaration caused JS parse error
|
- Fixed: Connect button not working — missing `updateSendButton()` function declaration caused JS parse error
|
||||||
- All UI event handlers now correctly initialized on app start
|
- All UI event handlers now correctly initialized on app start
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "ai.z.chat"
|
applicationId "ai.z.chat"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 5
|
versionCode 6
|
||||||
versionName "1.2.3"
|
versionName "1.2.4"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zai-chat",
|
"name": "zai-chat",
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
|
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -418,7 +418,8 @@ a:hover { text-decoration: underline; }
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
.code-header + pre { border-top-left-radius: 0; border-top-right-radius: 0; }
|
.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);
|
background: var(--accent-dim);
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
@@ -426,8 +427,51 @@ a:hover { text-decoration: underline; }
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
cursor: pointer;
|
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 {
|
.thinking-indicator {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -197,13 +197,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>About</h3>
|
<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 & GLM-5.1</p>
|
<p class="about-text">Built with Z.AI SDK & GLM-5.1</p>
|
||||||
<p class="about-text">Compatible with Android 15/16</p>
|
<p class="about-text">Compatible with Android 15/16</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Changelog</h3>
|
<h3>Changelog</h3>
|
||||||
<ul class="changelog-list">
|
<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>
|
<li>
|
||||||
<span class="changelog-version">v1.2.3</span>
|
<span class="changelog-version">v1.2.3</span>
|
||||||
<span class="changelog-date">2026-05-19</span>
|
<span class="changelog-date">2026-05-19</span>
|
||||||
|
|||||||
119
www/js/app.js
119
www/js/app.js
@@ -226,6 +226,40 @@
|
|||||||
return text.replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
|
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) {
|
function addCodeHeaders(container) {
|
||||||
container.querySelectorAll('pre code').forEach(function(block) {
|
container.querySelectorAll('pre code').forEach(function(block) {
|
||||||
var pre = block.parentElement;
|
var pre = block.parentElement;
|
||||||
@@ -233,7 +267,11 @@
|
|||||||
if (!pre.previousElementSibling || !pre.previousElementSibling.classList.contains('code-header')) {
|
if (!pre.previousElementSibling || !pre.previousElementSibling.classList.contains('code-header')) {
|
||||||
var header = document.createElement('div');
|
var header = document.createElement('div');
|
||||||
header.className = 'code-header';
|
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);
|
pre.parentElement.insertBefore(header, pre);
|
||||||
header.querySelector('.copy-btn').addEventListener('click', function() {
|
header.querySelector('.copy-btn').addEventListener('click', function() {
|
||||||
navigator.clipboard.writeText(block.textContent).then(function() {
|
navigator.clipboard.writeText(block.textContent).then(function() {
|
||||||
@@ -241,10 +279,48 @@
|
|||||||
setTimeout(function() { this.textContent = 'Copy'; }.bind(this), 2000);
|
setTimeout(function() { this.textContent = 'Copy'; }.bind(this), 2000);
|
||||||
}.bind(this));
|
}.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() {
|
function renderMessages() {
|
||||||
var container = $('#messages');
|
var container = $('#messages');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -270,6 +346,33 @@
|
|||||||
if (role === 'assistant') {
|
if (role === 'assistant') {
|
||||||
div.innerHTML = renderMarkdown(content);
|
div.innerHTML = renderMarkdown(content);
|
||||||
addCodeHeaders(div);
|
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 {
|
} else {
|
||||||
div.textContent = content;
|
div.textContent = content;
|
||||||
}
|
}
|
||||||
@@ -281,6 +384,20 @@
|
|||||||
function updateStreamingMessage(div, content) {
|
function updateStreamingMessage(div, content) {
|
||||||
div.innerHTML = renderMarkdown(content);
|
div.innerHTML = renderMarkdown(content);
|
||||||
addCodeHeaders(div);
|
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;
|
$('#messages').scrollTop = $('#messages').scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user