(function() {
'use strict';
var DEFAULT_BASE_URL = 'https://api.z.ai/api/coding/paas/v4';
var DEFAULT_MODEL = 'glm-5.1';
var STORAGE_KEY = 'zai_chat_';
var MODE_PROMPTS = {
chat: 'You are a helpful, knowledgeable AI assistant. Be concise and accurate.',
coding: 'You are an expert coding assistant. Write clean, efficient, well-documented code. Always use markdown code blocks with language tags. Explain your approach briefly before and after code. Handle edge cases and errors properly.',
brainstorm: 'You are a creative brainstorming partner. Generate diverse ideas, explore unconventional angles, build on concepts, and help evaluate trade-offs. Think freely and expansively. Present ideas in organized lists or tables when appropriate.',
agentic: 'You are an autonomous coding agent with direct terminal access on this Android device. You can write files, compile code, build APKs, and install apps locally. Use these tool formats:\n\n[CREATE_FILE path/to/file.ext]\nfile contents here\n[/CREATE_FILE]\n\n[RUN_COMMAND]\nshell command here\n[/RUN_COMMAND]\n\n[BUILD_APK project_name]\nbuilds Android project into installable APK\n[/BUILD_APK]\n\n[INSTALL_APK /path/to/file.apk]\ninstalls APK on this device\n[/INSTALL_APK]\n\nYou have access to: aapt2 (resource compiler), d8 (dex compiler), ecj (Java compiler), apksigner, and standard shell tools. Always: 1) Write all source files 2) Build step by step 3) Sign the APK 4) Offer to install it. When the user asks you to build an app, generate ALL files needed, build, sign, and provide the installable APK.'
};
var state = {
apiKey: '',
baseUrl: DEFAULT_BASE_URL,
model: DEFAULT_MODEL,
temperature: 0.7,
maxTokens: 4096,
streaming: true,
webSearch: false,
currentMode: 'chat',
theme: 'dark',
conversations: [],
activeConversationId: null,
isGenerating: false,
abortController: null,
streamingConvId: null,
streamingContent: '',
streamingResponseDiv: null,
terminalOpen: false,
keepAwake: false,
autoDeploy: true
};
function $(sel) { return document.querySelector(sel); }
function $$(sel) { return document.querySelectorAll(sel); }
function loadState() {
try {
state.apiKey = localStorage.getItem(STORAGE_KEY + 'apiKey') || '';
state.baseUrl = localStorage.getItem(STORAGE_KEY + 'baseUrl') || DEFAULT_BASE_URL;
state.model = localStorage.getItem(STORAGE_KEY + 'model') || DEFAULT_MODEL;
state.temperature = parseFloat(localStorage.getItem(STORAGE_KEY + 'temperature')) || 0.7;
state.maxTokens = parseInt(localStorage.getItem(STORAGE_KEY + 'maxTokens')) || 4096;
state.streaming = localStorage.getItem(STORAGE_KEY + 'streaming') !== 'false';
state.webSearch = localStorage.getItem(STORAGE_KEY + 'webSearch') === 'true';
state.currentMode = localStorage.getItem(STORAGE_KEY + 'currentMode') || 'chat';
state.theme = localStorage.getItem(STORAGE_KEY + 'theme') || 'dark';
state.terminalOpen = localStorage.getItem(STORAGE_KEY + 'terminalOpen') === 'true';
state.keepAwake = localStorage.getItem(STORAGE_KEY + 'keepAwake') === 'true';
state.autoDeploy = localStorage.getItem(STORAGE_KEY + 'autoDeploy') !== 'false';
var convData = localStorage.getItem(STORAGE_KEY + 'conversations');
state.conversations = convData ? JSON.parse(convData) : [];
state.activeConversationId = localStorage.getItem(STORAGE_KEY + 'activeConv') || null;
} catch(e) { console.error('Load state error:', e); }
}
function saveState() {
try {
localStorage.setItem(STORAGE_KEY + 'apiKey', state.apiKey);
localStorage.setItem(STORAGE_KEY + 'baseUrl', state.baseUrl);
localStorage.setItem(STORAGE_KEY + 'model', state.model);
localStorage.setItem(STORAGE_KEY + 'temperature', state.temperature.toString());
localStorage.setItem(STORAGE_KEY + 'maxTokens', state.maxTokens.toString());
localStorage.setItem(STORAGE_KEY + 'streaming', state.streaming.toString());
localStorage.setItem(STORAGE_KEY + 'webSearch', state.webSearch.toString());
localStorage.setItem(STORAGE_KEY + 'currentMode', state.currentMode);
localStorage.setItem(STORAGE_KEY + 'theme', state.theme);
localStorage.setItem(STORAGE_KEY + 'terminalOpen', state.terminalOpen.toString());
localStorage.setItem(STORAGE_KEY + 'keepAwake', state.keepAwake.toString());
localStorage.setItem(STORAGE_KEY + 'autoDeploy', state.autoDeploy.toString());
localStorage.setItem(STORAGE_KEY + 'conversations', JSON.stringify(state.conversations));
localStorage.setItem(STORAGE_KEY + 'activeConv', state.activeConversationId || '');
} catch(e) { console.error('Save state error:', e); }
}
function genId() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 9); }
function getConversation(id) {
var targetId = id || state.activeConversationId;
if (!targetId) return null;
return state.conversations.find(function(c) { return c.id === targetId; });
}
function flushStreamingToConversation() {
if (state.streamingConvId && state.streamingContent) {
var conv = getConversation(state.streamingConvId);
if (conv) {
var lastMsg = conv.messages[conv.messages.length - 1];
if (lastMsg && lastMsg.role === 'assistant' && lastMsg._streaming) {
lastMsg.content = state.streamingContent;
delete lastMsg._streaming;
} else {
conv.messages.push({ role: 'assistant', content: state.streamingContent });
}
saveState();
}
}
state.streamingConvId = null;
state.streamingContent = '';
state.streamingResponseDiv = null;
}
function newConversation() {
flushStreamingToConversation();
if (state.isGenerating) {
stopGeneration();
}
var conv = {
id: genId(),
title: 'New Chat',
mode: state.currentMode,
messages: [],
createdAt: Date.now()
};
state.conversations.unshift(conv);
state.activeConversationId = conv.id;
state.isGenerating = false;
state.abortController = null;
saveState();
renderConversationList();
renderMessages();
updateHeader();
updateSendButton();
updateTerminalVisibility();
}
function switchConversation(id) {
if (id === state.activeConversationId) {
closeSidebar();
return;
}
flushStreamingToConversation();
if (state.isGenerating) {
stopGeneration();
state.isGenerating = false;
state.abortController = null;
}
state.activeConversationId = id;
var conv = getConversation();
if (conv) {
state.currentMode = conv.mode || 'chat';
updateModeSelector();
}
saveState();
renderConversationList();
renderMessages();
updateHeader();
updateSendButton();
updateTerminalVisibility();
closeSidebar();
}
function deleteConversation(id) {
if (state.streamingConvId === id) {
flushStreamingToConversation();
if (state.isGenerating) stopGeneration();
state.isGenerating = false;
}
state.conversations = state.conversations.filter(function(c) { return c.id !== id; });
if (state.activeConversationId === id) {
state.activeConversationId = state.conversations.length > 0 ? state.conversations[0].id : null;
}
saveState();
renderConversationList();
renderMessages();
updateHeader();
updateTerminalVisibility();
}
function updateHeader() {
var conv = getConversation();
$('#conversation-title').textContent = conv ? conv.title : 'Z.AI Chat';
$('#current-mode-label').textContent = state.currentMode.charAt(0).toUpperCase() + state.currentMode.slice(1);
}
function showScreen(name) {
$$('.screen').forEach(function(s) { s.classList.remove('active'); });
$('#' + name + '-screen').classList.add('active');
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
function renderConversationList() {
var list = $('#conversation-list');
if (!list) return;
list.innerHTML = '';
state.conversations.forEach(function(conv) {
var div = document.createElement('div');
div.className = 'conv-item' + (conv.id === state.activeConversationId ? ' active' : '');
var msgCount = conv.messages.length;
div.innerHTML = '' + escapeHtml(conv.title) +
(msgCount > 0 ? ' (' + msgCount + ')' : '') +
'' +
'';
div.addEventListener('click', function(e) {
if (e.target.classList.contains('conv-delete')) {
e.stopPropagation();
deleteConversation(e.target.dataset.id);
return;
}
switchConversation(conv.id);
});
list.appendChild(div);
});
}
function escapeHtml(text) {
var d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
function renderMarkdown(text) {
if (typeof marked !== 'undefined') {
marked.setOptions({
highlight: function(code, lang) {
if (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) {
try { return hljs.highlight(code, { language: lang }).value; } catch(e) {}
}
return code;
},
breaks: true,
gfm: true
});
return marked.parse(text);
}
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;
var lang = (block.className.match(/language-(\w+)/) || [])[1] || 'code';
if (!pre.previousElementSibling || !pre.previousElementSibling.classList.contains('code-header')) {
var header = document.createElement('div');
header.className = 'code-header';
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() {
this.textContent = 'Copied!';
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;
container.innerHTML = '';
var conv = getConversation();
if (!conv || conv.messages.length === 0) {
container.innerHTML = 'Start a conversation with Z.AI
';
return;
}
conv.messages.forEach(function(msg) {
appendMessage(msg.role, msg.content, container, false);
});
container.scrollTop = container.scrollHeight;
updateTerminalContent();
}
function appendMessage(role, content, container, animate) {
container = container || $('#messages');
var div = document.createElement('div');
div.className = 'message ' + role;
if (animate === false) div.style.animation = 'none';
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);
});
if (state.currentMode === 'coding' || state.currentMode === 'agentic') {
var actions = parseAiActions(content);
addActionButtons(div, actions);
}
} else {
div.textContent = content;
}
container.appendChild(div);
container.scrollTop = container.scrollHeight;
return div;
}
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;
}
function showThinking() {
var container = $('#messages');
var div = document.createElement('div');
div.className = 'message assistant';
div.id = 'thinking-msg';
div.innerHTML = '';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function removeThinking() {
var el = $('#thinking-msg');
if (el) el.remove();
}
async function sendMessage() {
var input = $('#message-input');
var text = input.value.trim();
if (!text || state.isGenerating) return;
if (!state.apiKey) {
showScreen('setup');
return;
}
if (!state.activeConversationId) {
newConversation();
}
var conv = getConversation();
if (!conv) return;
conv.mode = state.currentMode;
if (conv.messages.length === 0) {
conv.title = text.substring(0, 50) + (text.length > 50 ? '...' : '');
updateHeader();
renderConversationList();
}
conv.messages.push({ role: 'user', content: text });
saveState();
input.value = '';
autoResize(input);
updateSendButton();
appendMessage('user', text);
state.isGenerating = true;
state.streamingConvId = conv.id;
state.streamingContent = '';
updateSendButton();
showThinking();
if (state.keepAwake) setWakeLock(true);
var requestBody = null;
var responseDiv = null;
try {
var systemPrompt = MODE_PROMPTS[state.currentMode] || MODE_PROMPTS.chat;
var apiMessages = [{ role: 'system', content: systemPrompt }];
conv.messages.forEach(function(m) {
if (m.role === 'user' || (m.role === 'assistant' && !m._streaming)) {
apiMessages.push({ role: m.role, content: m.content });
}
});
requestBody = {
model: state.model,
messages: apiMessages,
temperature: state.temperature,
max_tokens: state.maxTokens,
stream: state.streaming
};
if (state.webSearch) {
requestBody.tools = [{
type: 'web_search',
web_search: { search_query: text, search_result: true }
}];
}
removeThinking();
responseDiv = appendMessage('assistant', '');
state.streamingResponseDiv = responseDiv;
if (state.streaming) {
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 (err.name !== 'AbortError') {
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 {
state.isGenerating = false;
state.abortController = null;
state.streamingConvId = null;
state.streamingResponseDiv = null;
updateSendButton();
saveState();
updateTerminalContent();
if (state.keepAwake) setWakeLock(false);
if (state.autoDeploy && (state.currentMode === 'coding' || state.currentMode === 'agentic')) {
var finalContent = state.streamingContent || '';
if (!finalContent && conv && conv.messages.length) {
var last = conv.messages[conv.messages.length - 1];
if (last && last.role === 'assistant') finalContent = last.content;
}
if (finalContent) {
var autoActions = parseAiActions(finalContent);
if (autoActions.length > 0) {
autoExecuteActions(autoActions, conv);
}
}
}
}
}
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);
}
}
throw lastErr;
}
var _streamAutoSaveCounter = 0;
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 = '' +
'
' +
' Reconnecting... (attempt ' + (attempt + 2) + '/' + maxRetries + ')
';
$('#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;
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),
signal: state.abortController.signal
});
if (!resp.ok) {
var errData = {};
try { errData = await resp.json(); } catch(e) {}
throw new Error(errData.error?.message || 'API error ' + resp.status);
}
var reader = resp.body.getReader();
var decoder = new TextDecoder();
var fullContent = '';
var buffer = '';
_streamAutoSaveCounter = 0;
while (true) {
var chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
var lines = buffer.split('\n');
buffer = lines.pop() || '';
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line || !line.startsWith('data:')) continue;
var data = line.substring(5).trim();
if (data === '[DONE]') break;
try {
var parsed = JSON.parse(data);
var delta = parsed.choices && parsed.choices[0] && parsed.choices[0].delta;
if (delta && delta.content) {
fullContent += delta.content;
state.streamingContent = fullContent;
if (responseDiv && responseDiv.parentElement) {
updateStreamingMessage(responseDiv, fullContent);
}
_streamAutoSaveCounter++;
if (_streamAutoSaveCounter % 20 === 0) {
_saveStreamingProgress(conv, fullContent);
}
}
} catch(e) {}
}
}
conv.messages.push({ role: 'assistant', content: fullContent });
state.streamingContent = '';
}
function _saveStreamingProgress(conv, content) {
if (!conv) return;
var last = conv.messages[conv.messages.length - 1];
if (last && last.role === 'assistant' && last._streaming) {
last.content = content;
} else {
conv.messages.push({ role: 'assistant', content: content, _streaming: true });
}
saveState();
}
function stopGeneration() {
if (state.abortController) {
state.abortController.abort();
}
flushStreamingToConversation();
}
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 = '' +
'
' +
(isNet ? 'Connection interrupted' : escapeHtml(err.message || 'Request failed')) +
'
' +
(isNet ? '
' : '') +
'
';
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 = 'You are offline. Messages will be saved and sent when connection is restored.
';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
});
}
function updateSendButton() {
var input = $('#message-input');
var sendBtn = $('#send-btn');
var stopBtn = $('#stop-btn');
if (state.isGenerating) {
sendBtn.style.display = 'none';
stopBtn.style.display = 'flex';
} else {
sendBtn.style.display = 'flex';
stopBtn.style.display = 'none';
sendBtn.disabled = !input.value.trim();
}
}
function updateModeSelector() {
$$('.mode-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.mode === state.currentMode);
});
}
function openSidebar() {
$('#sidebar').classList.add('open');
$('#sidebar-overlay').classList.add('active');
}
function closeSidebar() {
$('#sidebar').classList.remove('open');
$('#sidebar-overlay').classList.remove('active');
}
function applyTheme(theme) {
state.theme = theme;
document.documentElement.setAttribute('data-theme', theme);
var headerBtn = $('#theme-toggle-header');
if (headerBtn) {
headerBtn.innerHTML = theme === 'dark' ? '☼' : '☾';
headerBtn.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
}
var settingsToggle = $('#settings-darkmode');
if (settingsToggle) settingsToggle.checked = (theme === 'dark');
var metaTheme = document.querySelector('meta[name="theme-color"]');
if (metaTheme) metaTheme.content = theme === 'dark' ? '#1a1a2e' : '#ffffff';
saveState();
}
function toggleTheme() {
applyTheme(state.theme === 'dark' ? 'light' : 'dark');
}
// ---- Terminal Panel ----
function parseTerminalEntries(content) {
if (!content) return [];
var entries = [];
var toolRegex = /\[(CREATE_FILE|EDIT_FILE|DELETE_FILE|RUN_COMMAND|SEARCH|READ_FILE|BUILD|TEST)\]\s*\(([^)]*)\)\s*\n?([\s\S]*?)(?=\n\[|$)/gi;
var match;
while ((match = toolRegex.exec(content)) !== null) {
entries.push({
type: 'tool',
action: match[1],
target: match[2].trim(),
body: match[3].trim()
});
}
var codeBlockRegex = /```(\w*)\n([\s\S]*?)```/gi;
var idx = 0;
while ((match = codeBlockRegex.exec(content)) !== null) {
var isTool = false;
for (var e = 0; e < entries.length; e++) {
if (entries[e].type === 'tool' && match.index >= content.indexOf(entries[e].body) - 20) {
isTool = true;
break;
}
}
if (!isTool) {
idx++;
var firstLine = match[2].trim().split('\n')[0];
var isFilePath = /^(\/|\.\/|\.\.\/|[A-Za-z]:\\|[a-zA-Z0-9_\-]+\.[a-zA-Z]{1,4}$)/.test(firstLine) && firstLine.length < 120 && firstLine.split('\n').length === 1;
entries.push({
type: 'code',
lang: match[1] || 'text',
code: match[2].trim(),
index: idx,
fileName: isFilePath ? firstLine : null
});
}
}
entries.sort(function(a, b) {
return content.indexOf(a.type === 'tool' ? '[' + a.action : '```' + (a.lang || '')) -
content.indexOf(b.type === 'tool' ? '[' + b.action : '```' + (b.lang || ''));
});
return entries;
}
function renderTerminalEntry(entry) {
if (entry.type === 'tool') {
var actionIcon = { CREATE_FILE: '+', EDIT_FILE: '~', DELETE_FILE: '-', RUN_COMMAND: '>', SEARCH: '?', READ_FILE: 'R', BUILD: 'B', TEST: 'T' };
var actionColor = { CREATE_FILE: 'var(--success)', EDIT_FILE: 'var(--warning)', DELETE_FILE: 'var(--danger)', RUN_COMMAND: 'var(--accent)', SEARCH: 'var(--text-secondary)', READ_FILE: 'var(--text-muted)', BUILD: 'var(--accent)', TEST: 'var(--success)' };
var icon = actionIcon[entry.action] || '>';
var color = actionColor[entry.action] || 'var(--accent)';
var html = '';
html += '';
if (entry.body) {
html += '
' + escapeHtml(entry.body.substring(0, 2000)) + (entry.body.length > 2000 ? '\n... (truncated)' : '') + '
';
}
html += '
';
return html;
}
if (entry.type === 'code') {
var label = entry.fileName ? escapeHtml(entry.fileName) : escapeHtml(entry.lang || 'code');
var displayCode = entry.fileName ? entry.code.split('\n').slice(1).join('\n') : entry.code;
if (!displayCode.trim()) displayCode = entry.code;
var html = '';
html += '';
html += '
' + escapeHtml(displayCode.substring(0, 3000)) + (displayCode.length > 3000 ? '\n... (truncated)' : '') + '
';
html += '
';
return html;
}
return '';
}
function updateTerminalContent() {
var termBody = $('#terminal-body');
if (!termBody) return;
termBody.innerHTML = '';
var conv = getConversation();
if (!conv) return;
var lastAssistant = null;
for (var i = conv.messages.length - 1; i >= 0; i--) {
if (conv.messages[i].role === 'assistant') {
lastAssistant = conv.messages[i];
break;
}
}
if (!lastAssistant && !state.streamingContent) {
termBody.innerHTML = 'No code output yet. Use Coding or Agentic mode to generate code.
';
return;
}
var content = state.streamingContent || (lastAssistant ? lastAssistant.content : '');
var entries = parseTerminalEntries(content);
if (entries.length === 0) {
termBody.innerHTML = 'No structured code blocks or tool calls detected in response.
';
return;
}
entries.forEach(function(entry) {
termBody.innerHTML += renderTerminalEntry(entry);
});
termBody.querySelectorAll('.term-copy-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var code = this.getAttribute('data-code').replace(/"/g, '"').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
navigator.clipboard.writeText(code).then(function() {
this.textContent = 'Copied!';
setTimeout(function() { this.textContent = 'Copy'; }.bind(this), 2000);
}.bind(this));
});
});
if (state.terminalOpen) {
termBody.scrollTop = termBody.scrollHeight;
}
}
function updateTerminalVisibility() {
var panel = $('#terminal-panel');
var toggleBtn = $('#terminal-toggle');
if (!panel || !toggleBtn) return;
var isDevMode = (state.currentMode === 'coding' || state.currentMode === 'agentic');
if (isDevMode) {
panel.style.display = 'flex';
toggleBtn.style.display = 'flex';
} else {
panel.style.display = 'none';
toggleBtn.style.display = 'none';
state.terminalOpen = false;
}
if (state.terminalOpen && isDevMode) {
panel.classList.add('open');
} else {
panel.classList.remove('open');
}
var label = toggleBtn.querySelector('.terminal-label');
if (label) label.textContent = state.terminalOpen ? 'Hide Terminal' : 'Show Terminal';
}
function toggleTerminal() {
state.terminalOpen = !state.terminalOpen;
var panel = $('#terminal-panel');
if (panel) panel.classList.toggle('open', state.terminalOpen);
var label = $('#terminal-toggle .terminal-label');
if (label) label.textContent = state.terminalOpen ? 'Hide Terminal' : 'Show Terminal';
if (state.terminalOpen) updateTerminalContent();
saveState();
}
// ---- Terminal & Shell System ----
var Shell = null;
var Installer = null;
var Wake = null;
var termState = {
history: [],
historyIndex: -1,
cwd: null,
homeDir: null,
toolsDir: null,
projectsDir: null,
isRunning: false,
activePid: null,
activeStreamId: null,
devToolsInstalled: false,
commandQueue: []
};
function initShellPlugins() {
try {
Shell = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Shell;
Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
Wake = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Wake;
} catch(e) {}
if (!Shell) console.warn('Shell plugin not available');
if (!Installer) console.warn('Installer plugin not available');
if (!Wake) console.warn('Wake plugin not available');
}
async function setWakeLock(on) {
if (!Wake) return;
try {
if (on) { await Wake.acquire(); } else { await Wake.release(); }
} catch(e) { console.warn('WakeLock error:', e); }
}
async function shellExec(command, cwd, stream) {
if (!Shell) return { output: '[Shell plugin not available]\n', exitCode: -1 };
try {
var opts = { command: command, stream: !!stream };
if (cwd) opts.cwd = cwd;
var result = await Shell.execute(opts);
return result;
} catch(e) {
return { output: '[Error: ' + e.message + ']\n', exitCode: -1 };
}
}
async function shellWriteFile(path, content) {
if (!Shell) return false;
try {
await Shell.writeFile({ path: path, content: content });
return true;
} catch(e) { return false; }
}
async function shellReadFile(path) {
if (!Shell) return null;
try {
var result = await Shell.readFile({ path: path });
return result.content;
} catch(e) { return null; }
}
async function shellMkdirs(path) {
if (!Shell) return false;
try { await Shell.mkdirs({ path: path }); return true; } catch(e) { return false; }
}
async function installApk(path) {
if (!Installer) { termPrint('[Installer plugin not available]', 'err'); return; }
try {
var result = await Installer.installApk({ path: path });
termPrint('[APK install triggered: ' + path + ']', 'success');
} catch(e) {
termPrint('[Install failed: ' + e.message + ']', 'err');
}
}
async function getDeviceInfo() {
if (!Installer) return {};
try { return await Installer.getDeviceInfo(); } catch(e) { return {}; }
}
function termPrint(text, className) {
var output = $('#term-output');
if (!output) return;
var line = document.createElement('div');
line.className = 'term-line' + (className ? ' term-' + className : '');
line.textContent = text;
output.appendChild(line);
output.scrollTop = output.scrollHeight;
}
function termPrintHtml(html, className) {
var output = $('#term-output');
if (!output) return;
var line = document.createElement('div');
line.className = 'term-line' + (className ? ' term-' + className : '');
line.innerHTML = html;
output.appendChild(line);
output.scrollTop = output.scrollHeight;
}
async function termExec(command) {
if (!command.trim()) return;
if (termState.isRunning) return;
termState.history.push(command);
termState.historyIndex = termState.history.length;
termPrint('$ ' + command, 'cmd');
var isCd = command.trim().startsWith('cd ');
termState.isRunning = true;
updateTermButtons();
var input = $('#term-input');
if (input) input.disabled = true;
try {
var result = await shellExec(command, termState.cwd, false);
if (result.output) {
termPrint(result.output.replace(/\n$/, ''), '');
}
if (result.exitCode !== 0 && result.exitCode !== undefined) {
termPrint('[exit code: ' + result.exitCode + ']', result.exitCode > 0 ? 'err' : '');
}
if (isCd && result.exitCode === 0) {
var target = command.trim().substring(3).trim();
if (target === '~' || target === '') {
termState.cwd = termState.homeDir;
} else {
var cwdResult = await shellExec('pwd', termState.cwd, false);
if (cwdResult.exitCode === 0 && cwdResult.output) {
termState.cwd = cwdResult.output.trim();
}
}
updateCwdDisplay();
}
} catch(e) {
termPrint('[Error: ' + e.message + ']', 'err');
} finally {
termState.isRunning = false;
updateTermButtons();
if (input) input.disabled = false;
if (input) input.focus();
}
}
async function termExecStreaming(command) {
if (!command.trim() || termState.isRunning) return;
termState.history.push(command);
termState.historyIndex = termState.history.length;
termPrint('$ ' + command, 'cmd');
termState.isRunning = true;
updateTermButtons();
var input = $('#term-input');
if (input) input.disabled = true;
try {
var result = await shellExec(command, termState.cwd, true);
termState.activePid = result.pid;
termState.activeStreamId = result.streamId;
if (Shell) {
Shell.addListener(result.streamId, function(event) {
if (event.data) {
termPrint(event.data.replace(/\n$/, ''), '');
}
if (event.done) {
termState.isRunning = false;
termState.activePid = null;
termState.activeStreamId = null;
updateTermButtons();
if (input) { input.disabled = false; input.focus(); }
processCommandQueue();
}
});
}
} catch(e) {
termPrint('[Error: ' + e.message + ']', 'err');
termState.isRunning = false;
updateTermButtons();
if (input) { input.disabled = false; input.focus(); }
processCommandQueue();
}
}
async function processCommandQueue() {
if (termState.commandQueue.length === 0 || termState.isRunning) return;
var next = termState.commandQueue.shift();
await termExec(next);
}
function termQueueCommand(command) {
if (termState.isRunning) {
termState.commandQueue.push(command);
} else {
termExec(command);
}
}
async function updateCwdDisplay() {
var display = $('#term-cwd-display');
if (!display) return;
if (!termState.cwd && Shell) {
try {
var env = await Shell.getEnv();
termState.cwd = env.CWD;
termState.homeDir = env.HOME;
termState.toolsDir = env.TOOLS;
termState.projectsDir = env.PROJECTS;
} catch(e) {}
}
var cwd = termState.cwd || '~';
if (termState.homeDir && cwd.startsWith(termState.homeDir)) {
cwd = '~' + cwd.substring(termState.homeDir.length);
}
display.textContent = cwd;
}
function updateTermButtons() {
var runBtn = $('#term-run-btn');
var stopBtn = $('#term-stop-btn');
if (runBtn) runBtn.style.display = termState.isRunning ? 'none' : 'flex';
if (stopBtn) stopBtn.style.display = termState.isRunning ? 'flex' : 'none';
}
// ---- AI Action Parser ----
function parseAiActions(content) {
var actions = [];
var createActionRegex = /\[CREATE_FILE\s+([^\]]+)\]\n([\s\S]*?)\[\/CREATE_FILE\]/gi;
var runCmdRegex = /\[RUN_COMMAND\]\n([\s\S]*?)\[\/RUN_COMMAND\]/gi;
var buildApkRegex = /\[BUILD_APK\s+([^\]]+)\]/gi;
var installApkRegex = /\[INSTALL_APK\s+([^\]]+)\]/gi;
var codeBlockFileRegex = /```(\w+)\s*\n([\s\S]*?)```/gi;
var match;
while ((match = createActionRegex.exec(content)) !== null) {
actions.push({ type: 'create_file', path: match[1].trim(), content: match[2] });
}
while ((match = runCmdRegex.exec(content)) !== null) {
actions.push({ type: 'run_command', command: match[1].trim() });
}
while ((match = buildApkRegex.exec(content)) !== null) {
actions.push({ type: 'build_apk', project: match[1].trim() });
}
while ((match = installApkRegex.exec(content)) !== null) {
actions.push({ type: 'install_apk', path: match[1].trim() });
}
while ((match = codeBlockFileRegex.exec(content)) !== null) {
var lang = match[1];
var code = match[2];
var firstLine = code.trim().split('\n')[0];
if (/^(\/|\.\/|\.\.\/|[A-Za-z]:\\)/.test(firstLine) && firstLine.length < 120 && /\.\w+$/.test(firstLine.split('\n')[0])) {
var filePath = firstLine.trim();
var fileContent = code.trim().split('\n').slice(1).join('\n');
actions.push({ type: 'create_file', path: filePath, content: fileContent });
}
}
return actions;
}
function addActionButtons(div, actions) {
if (actions.length === 0) return;
var hasFiles = actions.some(function(a) { return a.type === 'create_file'; });
var hasCommands = actions.some(function(a) { return a.type === 'run_command'; });
var hasBuild = actions.some(function(a) { return a.type === 'build_apk'; });
var hasInstall = actions.some(function(a) { return a.type === 'install_apk'; });
var actionBar = document.createElement('div');
actionBar.className = 'msg-actions';
if (hasFiles) {
var deployBtn = document.createElement('button');
deployBtn.className = 'deploy-btn';
deployBtn.innerHTML = '▶ Deploy Files';
deployBtn.addEventListener('click', function() { deployActions(actions); });
actionBar.appendChild(deployBtn);
}
if (hasBuild) {
var buildBtn = document.createElement('button');
buildBtn.className = 'deploy-btn';
buildBtn.style.background = 'linear-gradient(135deg, var(--accent), #a855f7)';
buildBtn.innerHTML = '📦 Build APK';
buildBtn.addEventListener('click', function() { buildFromActions(actions); });
actionBar.appendChild(buildBtn);
}
if (hasInstall) {
actions.forEach(function(action) {
if (action.type === 'install_apk') {
var installBtn = document.createElement('button');
installBtn.className = 'install-apk-btn';
installBtn.innerHTML = '📱 Install APK';
installBtn.addEventListener('click', function() { installApk(action.path); });
actionBar.appendChild(installBtn);
}
});
}
div.appendChild(actionBar);
}
async function deployActions(actions) {
showScreen('terminal');
termPrint('\n--- Deploying files ---', 'info');
for (var i = 0; i < actions.length; i++) {
var action = actions[i];
if (action.type === 'create_file') {
var path = action.path;
if (!path.startsWith('/')) {
path = (termState.projectsDir || termState.homeDir + '/projects') + '/' + path;
}
var dir = path.substring(0, path.lastIndexOf('/'));
await shellMkdirs(dir);
var ok = await shellWriteFile(path, action.content);
if (ok) {
termPrint(' [+] ' + path + ' (' + action.content.length + ' bytes)', 'success');
} else {
termPrint(' [!] Failed: ' + path, 'err');
}
}
}
termPrint('--- Deploy complete ---\n', 'info');
}
function showStatusToast(message, type) {
var existing = $('#status-toast');
if (existing) existing.remove();
var toast = document.createElement('div');
toast.id = 'status-toast';
toast.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:1000;' +
'padding:10px 20px;border-radius:20px;font-size:13px;font-weight:600;max-width:90%;' +
'text-align:center;pointer-events:none;opacity:0;transition:opacity 0.3s;' +
'background:' + (type === 'success' ? 'var(--success)' : type === 'err' ? 'var(--danger)' : 'var(--accent)') +
';color:white;box-shadow:0 4px 12px rgba(0,0,0,0.3)';
toast.textContent = message;
document.body.appendChild(toast);
requestAnimationFrame(function() { toast.style.opacity = '1'; });
setTimeout(function() {
toast.style.opacity = '0';
setTimeout(function() { if (toast.parentElement) toast.remove(); }, 300);
}, 3000);
}
async function autoExecuteActions(actions, conv) {
var hasFiles = actions.some(function(a) { return a.type === 'create_file'; });
var hasBuild = actions.some(function(a) { return a.type === 'build_apk'; });
var hasInstall = actions.some(function(a) { return a.type === 'install_apk'; });
var hasCommands = actions.some(function(a) { return a.type === 'run_command'; });
if (!hasFiles && !hasBuild && !hasInstall && !hasCommands) return;
var fileCount = actions.filter(function(a) { return a.type === 'create_file'; }).length;
if (fileCount > 0) {
showStatusToast('Auto-deploying ' + fileCount + ' file' + (fileCount > 1 ? 's' : '') + '...', 'info');
await deployActions(actions);
showStatusToast(fileCount + ' file' + (fileCount > 1 ? 's' : '') + ' deployed', 'success');
}
if (hasCommands) {
for (var i = 0; i < actions.length; i++) {
if (actions[i].type === 'run_command') {
showStatusToast('Running: ' + actions[i].command.substring(0, 40) + '...', 'info');
await termQueueCommand(actions[i].command);
}
}
}
if (hasBuild) {
showStatusToast('Building APK...', 'info');
await buildFromActions(actions);
}
if (hasInstall) {
for (var j = 0; j < actions.length; j++) {
if (actions[j].type === 'install_apk') {
showStatusToast('Installing APK...', 'info');
await installApk(actions[j].path);
}
}
}
}
async function buildFromActions(actions) {
showScreen('terminal');
termPrint('\n--- Building APK ---', 'info');
var projectDir = termState.projectsDir || (termState.homeDir + '/projects');
for (var i = 0; i < actions.length; i++) {
var action = actions[i];
if (action.type === 'create_file') {
var path = action.path;
if (!path.startsWith('/')) path = projectDir + '/' + path;
var dir = path.substring(0, path.lastIndexOf('/'));
await shellMkdirs(dir);
await shellWriteFile(path, action.content);
termPrint(' [+] ' + path, 'success');
}
}
termPrint('\nBuilding with aapt2 + d8...', 'info');
var buildCmd = 'cd ' + projectDir + ' && ' +
'if [ -d "app/src/main" ]; then ' +
' AAPT2=$(which aapt2 2>/dev/null || echo "") && ' +
' D8=$(which d8 2>/dev/null || echo "") && ' +
' ECJ=$(which ecj 2>/dev/null || echo "") && ' +
' if [ -z "$AAPT2" ]; then echo "[!] aapt2 not found. Run Setup Dev Tools first."; exit 1; fi && ' +
' echo "[*] Compiling resources..." && ' +
' $AAPT2 compile --dir app/src/main/res -o build/compiled_resources.zip 2>&1 && ' +
' echo "[*] Linking..." && ' +
' $AAPT2 link -o build/app.unsigned.apk ' +
' -I tools/android.jar ' +
' --manifest app/src/main/AndroidManifest.xml ' +
' -R build/compiled_resources.zip ' +
' --java build/gen 2>&1 && ' +
' echo "[*] Compiling Java..." && ' +
' find app/src/main/java -name "*.java" > build/sources.txt 2>/dev/null && ' +
' $ECJ -source 11 -target 11 -classpath tools/android.jar -d build/classes @build/sources.txt 2>&1 && ' +
' echo "[*] Converting to DEX..." && ' +
' $D8 --output build/ build/classes/**/*.class 2>&1 && ' +
' echo "[*] Packaging..." && ' +
' cd build && cp app.unsigned.apk app.unaligned.apk && ' +
' mkdir -p app.unaligned.apk.tmp && cd app.unaligned.apk.tmp && ' +
' unzip -o ../app.unaligned.apk && ' +
' cp ../classes.dex . && ' +
' zip -r ../app.unaligned.apk . && cd .. && rm -rf app.unaligned.apk.tmp && ' +
' echo "[*] Signing..." && ' +
' java -jar tools/uber-apk-signer.jar -a app.unaligned.apk --overwrite 2>&1 || ' +
' cp app.unaligned.apk app-signed.apk && ' +
' echo "[OK] APK built: ' + projectDir + '/build/app-signed.apk" && ' +
' echo "Size: $(du -h app-signed.apk | cut -f1)" ; ' +
'else echo "[!] No app/src/main found. Deploy files first."; fi';
await termExec(buildCmd);
}
// ---- Dev Tools Setup ----
var DEV_TOOLS = [
{ name: 'bash', url: 'https://github.com/termux/termux-packages/releases/download/bash-v5.2.21/bash-v5.2.21-aarch64.zip', type: 'binary' },
{ name: 'coreutils', url: 'https://github.com/termux/termux-packages/releases/download/coreutils-9.4/coreutils-9.4-aarch64.zip', type: 'binary' }
];
async function checkDevTools() {
if (!Shell) return false;
try {
var result = await shellExec('which aapt2 2>/dev/null && echo "OK" || echo "MISSING"', termState.homeDir, false);
termState.devToolsInstalled = result.output && result.output.indexOf('OK') >= 0;
return termState.devToolsInstalled;
} catch(e) { return false; }
}
async function setupDevTools() {
if (!Shell) {
alert('Shell plugin not available');
return;
}
var btn = $('#devsetup-install-btn');
var progress = $('#devsetup-progress');
var progressFill = $('#devsetup-progress-fill');
var progressText = $('#devsetup-progress-text');
var statusEl = $('#devsetup-status');
btn.disabled = true;
btn.querySelector('.btn-text').textContent = 'Installing...';
btn.querySelector('.btn-loader').style.display = 'inline-block';
progress.style.display = 'block';
var toolsDir = termState.toolsDir || (termState.homeDir + '/tools');
var binDir = toolsDir + '/bin';
var steps = [
{ label: 'Creating directories...', cmd: 'mkdir -p ' + binDir + ' ' + toolsDir + '/lib ' + toolsDir + '/java' },
{ label: 'Setting up shell...', cmd: 'cp /system/bin/sh ' + binDir + '/sh 2>/dev/null; chmod +x ' + binDir + '/* 2>/dev/null; echo OK' },
{ label: 'Checking environment...', cmd: 'ls -la ' + binDir + '/ && echo "Environment ready"' }
];
for (var i = 0; i < steps.length; i++) {
progressText.textContent = steps[i].label;
progressFill.style.width = ((i + 1) / (steps.length + 1) * 100) + '%';
var result = await shellExec(steps[i].cmd, termState.homeDir, false);
if (result.exitCode !== 0 && result.exitCode !== undefined) {
progressText.textContent = 'Warning: ' + steps[i].label + ' had issues';
}
}
progressText.textContent = 'Writing setup scripts...';
progressFill.style.width = '80%';
var setupScript = '#!/system/bin/sh\n' +
'TOOLS_DIR="' + toolsDir + '"\n' +
'BIN_DIR="' + binDir + '"\n' +
'echo "[*] Z.AI Dev Tools Setup"\n' +
'echo "[*] Tools directory: $TOOLS_DIR"\n' +
'echo "[*] For full build support, install these via Termux:"\n' +
'echo " pkg install aapt2 openjdk-17 dx ecj"\n' +
'echo ""\n' +
'echo "[*] Checking available tools..."\n' +
'for tool in aapt2 d8 ecj java apksigner zipalign; do\n' +
' if which $tool 2>/dev/null; then\n' +
' echo " [+] $tool: $(which $tool)"\n' +
' else\n' +
' echo " [-] $tool: not found"\n' +
' fi\n' +
'done\n' +
'echo ""\n' +
'echo "[*] For on-device APK building, you need:"\n' +
'echo " 1. Install Termux from F-Droid or GitHub"\n' +
'echo " 2. In Termux: pkg install aapt2 openjdk-17 dx ecj apksigner"\n' +
'echo " 3. Set TOOLS_PATH in terminal to point to Termux binaries"\n' +
'echo ""\n' +
'echo "[*] Device info:"\n' +
'uname -a\n' +
'echo "Arch: $(uname -m)"\n' +
'echo "[*] Done"\n';
await shellWriteFile(toolsDir + '/setup.sh', setupScript);
await shellExec('chmod +x ' + toolsDir + '/setup.sh', termState.homeDir, false);
var projectTemplate = '#!/system/bin/sh\n' +
'# Z.AI Quick Project Creator\n' +
'PROJECT_NAME="${1:-myapp}"\n' +
'PROJECT_DIR="' + (termState.projectsDir || termState.homeDir + '/projects') + '/$PROJECT_NAME"\n' +
'mkdir -p "$PROJECT_DIR"/app/src/main/java/ai/z/app\n' +
'mkdir -p "$PROJECT_DIR"/app/src/main/res/values\n' +
'mkdir -p "$PROJECT_DIR"/app/src/main/res/layout\n' +
'mkdir -p "$PROJECT_DIR"/app/src/main/res/mipmap-hdpi\n' +
'mkdir -p "$PROJECT_DIR"/build\n' +
'# AndroidManifest.xml\n' +
'cat > "$PROJECT_DIR"/app/src/main/AndroidManifest.xml << \'MANIFEST\'\n' +
'\n' +
'\n' +
' \n' +
' \n' +
' \n' +
' \n' +
' \n' +
' \n' +
' \n' +
' \n' +
'\n' +
'MANIFEST\n' +
'echo "[OK] Project created: $PROJECT_DIR"\n' +
'echo "[*] Next: Ask AI to generate the Java code, then build with Deploy"\n';
await shellWriteFile(toolsDir + '/create-project.sh', projectTemplate);
await shellExec('chmod +x ' + toolsDir + '/create-project.sh', termState.homeDir, false);
progressFill.style.width = '100%';
progressText.textContent = 'Setup complete!';
statusEl.innerHTML = 'Dev environment ready!
' +
'Use the terminal to build apps. Install Termux for full tool support (aapt2, d8, ecj).
';
btn.querySelector('.btn-text').textContent = 'Installed';
btn.querySelector('.btn-loader').style.display = 'none';
}
// ---- Init Terminal ----
function initTerminal() {
var termInput = $('#term-input');
var termRunBtn = $('#term-run-btn');
var termStopBtn = $('#term-stop-btn');
var termBackBtn = $('#term-back-btn');
var termSetupBtn = $('#term-setup-tools-btn');
var devsetupBtn = $('#devsetup-install-btn');
var devsetupBackBtn = $('#devsetup-back-btn');
if (termInput) {
termInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
var cmd = termInput.value;
termInput.value = '';
termExec(cmd);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (termState.historyIndex > 0) {
termState.historyIndex--;
termInput.value = termState.history[termState.historyIndex] || '';
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (termState.historyIndex < termState.history.length - 1) {
termState.historyIndex++;
termInput.value = termState.history[termState.historyIndex] || '';
} else {
termState.historyIndex = termState.history.length;
termInput.value = '';
}
}
});
}
if (termRunBtn) {
termRunBtn.addEventListener('click', function() {
var cmd = termInput.value;
termInput.value = '';
termExec(cmd);
});
}
if (termStopBtn) {
termStopBtn.addEventListener('click', async function() {
if (Shell && termState.activePid) {
try { await Shell.kill({ pid: termState.activePid }); } catch(e) {}
}
termState.isRunning = false;
termState.activePid = null;
updateTermButtons();
termPrint('[Process killed]', 'warning');
if (termInput) { termInput.disabled = false; termInput.focus(); }
});
}
if (termBackBtn) {
termBackBtn.addEventListener('click', function() {
showScreen('chat');
});
}
if (termSetupBtn) {
termSetupBtn.addEventListener('click', function() {
showScreen('devsetup');
});
}
if (devsetupBtn) {
devsetupBtn.addEventListener('click', function() {
setupDevTools();
});
}
if (devsetupBackBtn) {
devsetupBackBtn.addEventListener('click', function() {
showScreen('terminal');
});
}
$$('.term-quick-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var cmd = this.dataset.cmd;
if (termInput) {
termInput.value = cmd;
termInput.focus();
if (!cmd.endsWith(' ')) {
termExec(cmd);
termInput.value = '';
}
}
});
});
updateCwdDisplay();
if (Shell) {
Shell.getEnv().then(function(env) {
termState.homeDir = env.HOME;
termState.toolsDir = env.TOOLS;
termState.projectsDir = env.PROJECTS;
termState.cwd = env.CWD || env.HOME;
updateCwdDisplay();
termPrint('Z.AI Terminal v1.3.0', 'info');
termPrint('Home: ' + termState.homeDir, 'info');
termPrint('Type "help" for commands, "setup" for dev tools\n', 'info');
}).catch(function() {});
}
}
// ---- Terminal command handler ----
var origTermExec = termExec;
termExec = async function(command) {
if (!command.trim()) return;
var lower = command.trim().toLowerCase();
if (lower === 'help') {
termPrint('$ help', 'cmd');
termPrint('Z.AI Terminal Commands:', 'info');
termPrint(' help - Show this help', '');
termPrint(' setup - Open dev tools setup', '');
termPrint(' sysinfo - Show device info', '');
termPrint(' create NAME - Create new Android project', '');
termPrint(' install APK - Install an APK file', '');
termPrint(' clear - Clear terminal', '');
termPrint(' exit - Back to chat', '');
termPrint('', '');
termPrint('Shell: Any standard Linux command works here.', '');
termPrint('Tip: Use "setup" to install build tools (aapt2, d8, ecj)\n', '');
return;
}
if (lower === 'setup') {
showScreen('devsetup');
return;
}
if (lower === 'sysinfo') {
termPrint('$ sysinfo', 'cmd');
var info = await getDeviceInfo();
termPrint('Device: ' + (info.manufacturer || '') + ' ' + (info.model || ''), '');
termPrint('Android: ' + (info.release || '?') + ' (SDK ' + (info.sdk || '?') + ')', '');
termPrint('ABI: ' + (info.abi || '?'), '');
termPrint('Files: ' + (info.filesDir || '?'), '');
termPrint('Package: ' + (info.package || '?') + '\n', '');
return;
}
if (lower.startsWith('create ')) {
var name = command.trim().substring(7).trim();
termPrint('$ create ' + name, 'cmd');
var projectDir = termState.projectsDir || (termState.homeDir + '/projects');
await shellExec('sh ' + termState.toolsDir + '/setup.sh', termState.homeDir, false);
await shellExec('sh ' + termState.toolsDir + '/create-project.sh ' + name, termState.homeDir, false);
return;
}
if (lower.startsWith('install ')) {
var path = command.trim().substring(8).trim();
termPrint('$ install ' + path, 'cmd');
await installApk(path);
return;
}
if (lower === 'clear') {
var output = $('#term-output');
if (output) output.innerHTML = '';
return;
}
if (lower === 'exit') {
showScreen('chat');
return;
}
await origTermExec(command);
};
// ---- Rest of init ----
async function testConnection(apiKey, baseUrl) {
var url = (baseUrl || state.baseUrl).replace(/\/+$/, '') + '/chat/completions';
var resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey,
'Accept-Language': 'en-US,en'
},
body: JSON.stringify({
model: state.model,
messages: [{ role: 'user', content: 'Hi' }],
max_tokens: 10
})
});
if (!resp.ok) {
var errData = {};
try { errData = await resp.json(); } catch(e) {}
throw new Error(errData.error?.message || 'Connection failed (' + resp.status + ')');
}
return await resp.json();
}
function populateSettings() {
$('#settings-token').value = state.apiKey;
$('#settings-url').value = state.baseUrl;
$('#settings-model').value = state.model;
$('#settings-temp').value = state.temperature;
$('#temp-value').textContent = state.temperature;
$('#settings-tokens').value = state.maxTokens;
$('#tokens-value').textContent = state.maxTokens;
$('#settings-websearch').checked = state.webSearch;
$('#settings-streaming').checked = state.streaming;
$('#settings-autodeploy').checked = state.autoDeploy;
$('#settings-keepawake').checked = state.keepAwake;
}
function saveSettings() {
state.apiKey = $('#settings-token').value.trim();
state.baseUrl = $('#settings-url').value.trim();
state.model = $('#settings-model').value;
state.temperature = parseFloat($('#settings-temp').value);
state.maxTokens = parseInt($('#settings-tokens').value);
state.webSearch = $('#settings-websearch').checked;
state.streaming = $('#settings-streaming').checked;
saveState();
}
function exportConversations() {
var data = JSON.stringify(state.conversations, null, 2);
var blob = new Blob([data], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'zai-chat-export-' + new Date().toISOString().slice(0, 10) + '.json';
a.click();
URL.revokeObjectURL(url);
}
function init() {
loadState();
initShellPlugins();
if (state.apiKey) {
showScreen('chat');
if (state.activeConversationId) {
var conv = getConversation();
if (conv) {
state.currentMode = conv.mode || 'chat';
}
}
renderConversationList();
renderMessages();
updateHeader();
updateModeSelector();
updateTerminalVisibility();
$('#api-token').value = state.apiKey;
$('#base-url').value = state.baseUrl;
}
$('#connect-btn').addEventListener('click', async function() {
var btn = this;
var apiKey = $('#api-token').value.trim();
var baseUrl = $('#base-url').value;
var errorEl = $('#setup-error');
if (!apiKey) {
errorEl.textContent = 'Please enter your API key';
errorEl.style.display = 'block';
return;
}
btn.disabled = true;
btn.querySelector('.btn-text').textContent = 'Connecting...';
btn.querySelector('.btn-loader').style.display = 'inline-block';
errorEl.style.display = 'none';
try {
await testConnection(apiKey, baseUrl);
state.apiKey = apiKey;
state.baseUrl = baseUrl;
saveState();
showScreen('chat');
newConversation();
} catch(err) {
errorEl.textContent = err.message;
errorEl.style.display = 'block';
} finally {
btn.disabled = false;
btn.querySelector('.btn-text').textContent = 'Connect';
btn.querySelector('.btn-loader').style.display = 'none';
}
});
$('#api-token').addEventListener('keydown', function(e) {
if (e.key === 'Enter') $('#connect-btn').click();
});
$('#message-input').addEventListener('input', function() {
autoResize(this);
updateSendButton();
});
$('#message-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
$('#send-btn').addEventListener('click', sendMessage);
$('#stop-btn').addEventListener('click', stopGeneration);
$$('.mode-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var mode = this.dataset.mode;
if (mode === 'terminal') {
showScreen('terminal');
return;
}
state.currentMode = mode;
updateModeSelector();
updateHeader();
updateTerminalVisibility();
saveState();
});
});
$('#menu-btn').addEventListener('click', openSidebar);
$('#sidebar-close').addEventListener('click', closeSidebar);
$('#sidebar-overlay').addEventListener('click', closeSidebar);
$('#new-chat-btn').addEventListener('click', function() { newConversation(); });
$('#new-chat-sidebar').addEventListener('click', function() { newConversation(); closeSidebar(); });
$('#settings-btn').addEventListener('click', function() {
populateSettings();
showScreen('settings');
});
$('#settings-back').addEventListener('click', function() {
saveSettings();
showScreen('chat');
});
$('#settings-temp').addEventListener('input', function() {
$('#temp-value').textContent = this.value;
});
$('#settings-tokens').addEventListener('input', function() {
$('#tokens-value').textContent = this.value;
});
$('#settings-token').addEventListener('change', saveSettings);
$('#settings-url').addEventListener('change', saveSettings);
$('#settings-model').addEventListener('change', saveSettings);
$('#settings-websearch').addEventListener('change', saveSettings);
$('#settings-streaming').addEventListener('change', saveSettings);
$('#settings-autodeploy').addEventListener('change', function() {
state.autoDeploy = this.checked;
saveState();
});
$('#settings-keepawake').addEventListener('change', function() {
state.keepAwake = this.checked;
saveState();
});
$('#theme-toggle-header').addEventListener('click', toggleTheme);
$('#settings-darkmode').addEventListener('change', function() {
applyTheme(this.checked ? 'dark' : 'light');
});
$('#terminal-toggle').addEventListener('click', toggleTerminal);
$('#export-btn').addEventListener('click', exportConversations);
$('#clear-btn').addEventListener('click', function() {
if (confirm('Clear all conversations? This cannot be undone.')) {
state.conversations = [];
state.activeConversationId = null;
saveState();
renderConversationList();
renderMessages();
updateHeader();
updateTerminalContent();
}
});
updateModeSelector();
updateSendButton();
applyTheme(state.theme);
setupVisibilityHandler();
initTerminal();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();