(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. Break down complex tasks into clear steps. Write production-quality code with proper error handling, tests, and documentation. Think through the architecture before coding. Use tool-calling format when appropriate: [SEARCH], [CREATE_FILE], [EDIT_FILE], [RUN_COMMAND]. Always verify your work.'
};
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
};
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';
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 + '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, '
');
}
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';
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));
});
}
});
}
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);
} else {
div.textContent = content;
}
container.appendChild(div);
container.scrollTop = container.scrollHeight;
return div;
}
function updateStreamingMessage(div, content) {
div.innerHTML = renderMarkdown(content);
addCodeHeaders(div);
$('#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();
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();
}
}
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();
}
// ---- 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;
}
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();
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() {
state.currentMode = this.dataset.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);
$('#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();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();