v1.2.2 - Fix network error on background, auto-retry streaming with reconnect

This commit is contained in:
admin
2026-05-19 15:50:45 +04:00
Unverified
parent 2e327317e4
commit 1026259a20
3831 changed files with 384316 additions and 39 deletions

View File

@@ -811,6 +811,19 @@ a:hover { text-decoration: underline; }
background: transparent;
}
.retry-btn {
background: var(--accent);
color: white;
border: none;
padding: 8px 24px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition);
}
.retry-btn:hover { background: var(--accent-hover); transform: scale(1.05); }
.terminal-toggle-btn {
display: none;
align-items: center;

View File

@@ -197,13 +197,26 @@
</div>
<div class="settings-section">
<h3>About</h3>
<p class="about-text">Z.AI Chat v1.2.1</p>
<p class="about-text">Z.AI Chat v1.2.2</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.2</span>
<span class="changelog-date">2026-05-19</span>
<ul>
<li>Fixed: network error on app backgrounding — now auto-retries with reconnect</li>
<li>Streaming resumes from last saved token after reconnection</li>
<li>Exponential backoff retry (3 attempts) for network interruptions</li>
<li>Retry button shown for failed requests due to connectivity</li>
<li>Offline/online detection with status banner</li>
<li>Visibility change handler saves state on app background</li>
<li>Partial response preserved and restorable on reconnect</li>
</ul>
</li>
<li>
<span class="changelog-version">v1.2.1</span>
<span class="changelog-date">2026-05-19</span>

View File

@@ -337,6 +337,9 @@
updateSendButton();
showThinking();
var requestBody = null;
var responseDiv = null;
try {
var systemPrompt = MODE_PROMPTS[state.currentMode] || MODE_PROMPTS.chat;
var apiMessages = [{ role: 'system', content: systemPrompt }];
@@ -346,7 +349,7 @@
}
});
var requestBody = {
requestBody = {
model: state.model,
messages: apiMessages,
temperature: state.temperature,
@@ -362,13 +365,13 @@
}
removeThinking();
var responseDiv = appendMessage('assistant', '');
responseDiv = appendMessage('assistant', '');
state.streamingResponseDiv = responseDiv;
if (state.streaming) {
await streamResponse(requestBody, responseDiv, conv);
await streamResponseWithRetry(requestBody, responseDiv, conv);
} else {
var result = await apiRequest(requestBody);
var result = await apiRequestWithRetry(requestBody);
var content = result.choices[0].message.content;
updateStreamingMessage(responseDiv, content);
state.streamingContent = content;
@@ -377,9 +380,11 @@
} catch(err) {
removeThinking();
if (err.name !== 'AbortError') {
appendMessage('system', 'Error: ' + (err.message || 'Request failed'));
}
if (state.streamingContent) {
if (state.streamingContent) {
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
}
var retryDiv = appendRetryMessage(err, requestBody, conv);
} else if (state.streamingContent) {
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
}
} finally {
@@ -393,28 +398,97 @@
}
}
async function apiRequest(body) {
var url = state.baseUrl.replace(/\/+$/, '') + '/chat/completions';
var resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + state.apiKey,
'Accept-Language': 'en-US,en'
},
body: JSON.stringify(body)
});
if (!resp.ok) {
var errData = {};
try { errData = await resp.json(); } catch(e) {}
throw new Error(errData.error?.message || 'API error ' + resp.status);
function isNetworkError(err) {
if (!err) return false;
var msg = (err.message || '').toLowerCase();
var name = (err.name || '').toLowerCase();
return name === 'typeerror' || name === 'networkerror' ||
msg.indexOf('failed to fetch') >= 0 ||
msg.indexOf('network') >= 0 ||
msg.indexOf('load failed') >= 0 ||
msg.indexOf('connection') >= 0 ||
msg.indexOf('net::') >= 0 ||
msg.indexOf('interrupted') >= 0;
}
function sleep(ms) {
return new Promise(function(r) { setTimeout(r, ms); });
}
async function apiRequestWithRetry(body, maxRetries) {
maxRetries = maxRetries || 3;
var lastErr;
for (var attempt = 0; attempt < maxRetries; attempt++) {
try {
return await apiRequest(body);
} catch(err) {
lastErr = err;
if (!isNetworkError(err) || attempt >= maxRetries - 1) throw err;
var delay = 1000 * Math.pow(2, attempt);
appendMessage('system', 'Connection lost. Retrying in ' + (delay / 1000) + 's... (attempt ' + (attempt + 2) + '/' + maxRetries + ')');
await sleep(delay);
}
}
return await resp.json();
throw lastErr;
}
var _streamAutoSaveCounter = 0;
async function streamResponse(body, responseDiv, conv) {
async function streamResponseWithRetry(body, responseDiv, conv, maxRetries) {
maxRetries = maxRetries || 3;
var lastErr;
for (var attempt = 0; attempt < maxRetries; attempt++) {
try {
await streamResponse(body, responseDiv, conv, attempt > 0);
return;
} catch(err) {
lastErr = err;
if (err.name === 'AbortError') throw err;
if (!isNetworkError(err)) throw err;
if (attempt >= maxRetries - 1) throw err;
if (state.streamingContent) {
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: true });
saveState();
}
var delay = 1500 * Math.pow(2, attempt);
var retryNotice = document.createElement('div');
retryNotice.className = 'message system';
retryNotice.innerHTML = '<div style="display:flex;align-items:center;gap:6px;justify-content:center">' +
'<div class="thinking-dots"><span></span><span></span><span></span></div>' +
' Reconnecting... (attempt ' + (attempt + 2) + '/' + maxRetries + ')</div>';
$('#messages').appendChild(retryNotice);
$('#messages').scrollTop = $('#messages').scrollHeight;
await sleep(delay);
if (retryNotice.parentElement) retryNotice.remove();
var lastAssistant = '';
for (var mi = conv.messages.length - 1; mi >= 0; mi--) {
if (conv.messages[mi].role === 'assistant' && !conv.messages[mi]._streaming) {
lastAssistant = conv.messages[mi].content;
break;
}
}
body.messages = body.messages.filter(function(m) { return m.role !== 'assistant'; });
if (state.streamingContent) {
body.messages.push({ role: 'assistant', content: state.streamingContent });
}
body.stream = true;
if (responseDiv && responseDiv.parentElement) {
var currentText = state.streamingContent || lastAssistant;
updateStreamingMessage(responseDiv, currentText + '\n\n*--- connection interrupted, resuming ---*\n');
state.streamingContent = currentText;
}
}
}
throw lastErr;
}
async function streamResponse(body, responseDiv, conv, isRetry) {
state.abortController = new AbortController();
body.stream = true;
@@ -496,7 +570,111 @@
flushStreamingToConversation();
}
function updateSendButton() {
async function apiRequest(body) {
var url = state.baseUrl.replace(/\/+$/, '') + '/chat/completions';
var resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + state.apiKey,
'Accept-Language': 'en-US,en'
},
body: JSON.stringify(body)
});
if (!resp.ok) {
var errData = {};
try { errData = await resp.json(); } catch(e) {}
throw new Error(errData.error?.message || 'API error ' + resp.status);
}
return await resp.json();
}
function appendRetryMessage(err, requestBody, conv) {
var container = $('#messages');
var div = document.createElement('div');
div.className = 'message system';
var isNet = isNetworkError(err);
div.innerHTML = '<div style="text-align:center">' +
'<div style="color:var(--danger);margin-bottom:8px">' +
(isNet ? 'Connection interrupted' : escapeHtml(err.message || 'Request failed')) +
'</div>' +
(isNet ? '<button class="retry-btn" id="retry-btn">Retry</button>' : '') +
'</div>';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
if (isNet) {
setTimeout(function() {
var btn = $('#retry-btn');
if (btn) btn.addEventListener('click', function() {
if (div.parentElement) div.remove();
retryLastRequest(requestBody, conv);
});
}, 50);
}
}
async function retryLastRequest(requestBody, conv) {
if (!requestBody || !conv) return;
state.isGenerating = true;
state.streamingConvId = conv.id;
updateSendButton();
var responseDiv = appendMessage('assistant', '');
state.streamingResponseDiv = responseDiv;
try {
if (state.streaming) {
requestBody.stream = true;
await streamResponseWithRetry(requestBody, responseDiv, conv);
} else {
var result = await apiRequestWithRetry(requestBody);
var content = result.choices[0].message.content;
updateStreamingMessage(responseDiv, content);
state.streamingContent = content;
conv.messages.push({ role: 'assistant', content: content });
}
} catch(err) {
removeThinking();
if (state.streamingContent) {
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
}
appendRetryMessage(err, requestBody, conv);
} finally {
state.isGenerating = false;
state.abortController = null;
state.streamingConvId = null;
state.streamingResponseDiv = null;
updateSendButton();
saveState();
updateTerminalContent();
}
}
function setupVisibilityHandler() {
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
flushStreamingToConversation();
}
});
window.addEventListener('online', function() {
var msg = $('#offline-msg');
if (msg) msg.remove();
});
window.addEventListener('offline', function() {
var container = $('#messages');
if (container && !$('#offline-msg')) {
var div = document.createElement('div');
div.className = 'message system';
div.id = 'offline-msg';
div.innerHTML = '<div style="color:var(--warning)">You are offline. Messages will be saved and sent when connection is restored.</div>';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
});
}
var input = $('#message-input');
var sendBtn = $('#send-btn');
var stopBtn = $('#stop-btn');
@@ -906,6 +1084,7 @@
updateModeSelector();
updateSendButton();
applyTheme(state.theme);
setupVisibilityHandler();
}
if (document.readyState === 'loading') {