v1.2.0 - Z.AI Chat for Android with dark/light themes, 4 chat modes, streaming SSE, coding plan endpoint

This commit is contained in:
admin
2026-05-19 15:17:22 +04:00
Unverified
commit d62a850ac5
67 changed files with 5593 additions and 0 deletions

702
www/css/styles.css Normal file
View File

@@ -0,0 +1,702 @@
:root,
[data-theme="dark"] {
--bg-primary: #0f0f23;
--bg-secondary: #1a1a2e;
--bg-tertiary: #16213e;
--bg-input: #1e1e3a;
--bg-msg-user: #2d2d5e;
--bg-msg-ai: #1a1a2e;
--bg-code: #0d0d1a;
--text-primary: #e0e0ff;
--text-secondary: #8888aa;
--text-muted: #555577;
--accent: #6c63ff;
--accent-hover: #7f78ff;
--accent-dim: rgba(108, 99, 255, 0.15);
--border: #2a2a4a;
--danger: #ff4757;
--success: #2ed573;
--warning: #ffa502;
--shadow: rgba(0, 0, 0, 0.3);
--radius: 16px;
--radius-sm: 10px;
--transition: 0.2s ease;
--sidebar-overlay-bg: rgba(0,0,0,0.5);
--logo-shadow: rgba(108, 99, 255, 0.3);
--scrollbar-thumb-hover: #555577;
}
[data-theme="light"] {
--bg-primary: #f5f5fa;
--bg-secondary: #ffffff;
--bg-tertiary: #eef0f6;
--bg-input: #eef0f6;
--bg-msg-user: #e0e3ff;
--bg-msg-ai: #ffffff;
--bg-code: #f0f1f5;
--text-primary: #1a1a2e;
--text-secondary: #6b6b8a;
--text-muted: #9999aa;
--accent: #6c63ff;
--accent-hover: #5a52e0;
--accent-dim: rgba(108, 99, 255, 0.1);
--border: #d8dae6;
--danger: #e03e4d;
--success: #22b85c;
--warning: #e09500;
--shadow: rgba(0, 0, 0, 0.08);
--sidebar-overlay-bg: rgba(0,0,0,0.3);
--logo-shadow: rgba(108, 99, 255, 0.2);
--scrollbar-thumb-hover: #aaaacc;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
-webkit-font-smoothing: antialiased;
}
#app { height: 100%; display: flex; flex-direction: column; }
.screen { display: none; height: 100%; flex-direction: column; }
.screen.active { display: flex; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.btn-primary {
background: var(--accent);
color: white;
border: none;
padding: 14px 32px;
border-radius: var(--radius-sm);
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
}
.btn-primary:hover { background: var(--accent-hover); transform: translateY(-1px); }
.btn-primary:active { transform: translateY(0); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn-secondary {
background: transparent;
color: var(--accent);
border: 1px solid var(--accent);
padding: 10px 20px;
border-radius: var(--radius-sm);
font-size: 14px;
cursor: pointer;
transition: all var(--transition);
width: 100%;
}
.btn-secondary:hover { background: var(--accent-dim); }
.btn-danger {
background: transparent;
color: var(--danger);
border: 1px solid var(--danger);
padding: 10px 20px;
border-radius: var(--radius-sm);
font-size: 14px;
cursor: pointer;
transition: all var(--transition);
width: 100%;
}
.btn-danger:hover { background: rgba(255, 71, 87, 0.1); }
.icon-btn {
background: none;
border: none;
color: var(--text-primary);
font-size: 22px;
cursor: pointer;
padding: 8px;
border-radius: 8px;
transition: background var(--transition);
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
min-height: 40px;
}
.icon-btn:hover { background: var(--accent-dim); }
.input-group {
margin-bottom: 16px;
display: flex;
flex-direction: column;
gap: 6px;
}
.input-group label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.input-group input[type="text"],
.input-group input[type="password"],
.input-group select {
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 12px 16px;
border-radius: var(--radius-sm);
font-size: 15px;
outline: none;
transition: border-color var(--transition);
}
.input-group input:focus,
.input-group select:focus { border-color: var(--accent); }
.input-group input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 6px;
border-radius: 3px;
background: var(--border);
outline: none;
}
.input-group input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
}
.input-hint { font-size: 12px; color: var(--text-muted); }
.toggle-group { flex-direction: row; align-items: center; justify-content: space-between; }
.toggle { position: relative; display: inline-block; width: 48px; height: 26px; }
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--border);
border-radius: 26px;
transition: var(--transition);
}
.toggle-slider:before {
content: "";
position: absolute;
height: 20px; width: 20px;
left: 3px; bottom: 3px;
background: white;
border-radius: 50%;
transition: var(--transition);
}
.toggle input:checked + .toggle-slider { background: var(--accent); }
.toggle input:checked + .toggle-slider:before { transform: translateX(22px); }
.error-msg {
color: var(--danger);
font-size: 13px;
text-align: center;
padding: 8px;
background: rgba(255, 71, 87, 0.1);
border-radius: var(--radius-sm);
}
.btn-loader {
width: 18px; height: 18px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Setup Screen */
#setup-screen {
justify-content: center;
align-items: center;
padding: 20px;
background: linear-gradient(135deg, var(--bg-primary), var(--bg-secondary), var(--bg-tertiary));
}
.setup-container {
width: 100%;
max-width: 420px;
display: flex;
flex-direction: column;
gap: 32px;
}
.logo-area { text-align: center; }
.logo-icon {
width: 80px; height: 80px;
background: linear-gradient(135deg, var(--accent), #a855f7);
border-radius: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 36px;
font-weight: 800;
color: white;
margin-bottom: 16px;
box-shadow: 0 8px 32px var(--logo-shadow);
}
.logo-area h1 { font-size: 28px; font-weight: 700; }
.subtitle { color: var(--text-secondary); margin-top: 4px; font-size: 14px; }
.setup-form {
background: var(--bg-secondary);
padding: 24px;
border-radius: var(--radius);
border: 1px solid var(--border);
}
.setup-modes h3 {
font-size: 14px;
color: var(--text-secondary);
text-align: center;
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 1px;
}
.mode-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.mode-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 14px 10px;
text-align: center;
display: flex;
flex-direction: column;
gap: 4px;
}
.mode-icon { font-size: 24px; }
.mode-name { font-weight: 600; font-size: 14px; }
.mode-desc { font-size: 11px; color: var(--text-muted); }
/* Chat Screen */
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
min-height: 56px;
position: relative;
z-index: 10;
}
.header-left, .header-right { display: flex; align-items: center; gap: 4px; }
.header-title h2 { font-size: 16px; font-weight: 600; line-height: 1.2; }
.mode-label {
font-size: 10px;
padding: 2px 8px;
border-radius: 10px;
background: var(--accent-dim);
color: var(--accent);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
.message {
max-width: 88%;
padding: 12px 16px;
border-radius: var(--radius);
line-height: 1.6;
font-size: 15px;
word-wrap: break-word;
overflow-wrap: break-word;
position: relative;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.message.user {
background: var(--bg-msg-user);
align-self: flex-end;
border-bottom-right-radius: 4px;
}
.message.assistant {
background: var(--bg-msg-ai);
border: 1px solid var(--border);
align-self: flex-start;
border-bottom-left-radius: 4px;
}
.message.system {
background: var(--bg-tertiary);
align-self: center;
text-align: center;
font-size: 13px;
color: var(--text-secondary);
max-width: 90%;
border-radius: var(--radius-sm);
}
.message.assistant pre {
background: var(--bg-code);
border-radius: 8px;
padding: 12px;
overflow-x: auto;
margin: 8px 0;
position: relative;
border: 1px solid var(--border);
}
.message.assistant code {
font-family: 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace;
font-size: 13px;
line-height: 1.5;
}
.message.assistant :not(pre) > code {
background: var(--bg-code);
padding: 2px 6px;
border-radius: 4px;
font-size: 13px;
}
.message.assistant p { margin: 6px 0; }
.message.assistant ul, .message.assistant ol { margin: 6px 0; padding-left: 20px; }
.message.assistant li { margin: 3px 0; }
.message.assistant h1, .message.assistant h2, .message.assistant h3, .message.assistant h4 {
margin: 12px 0 6px;
}
.message.assistant blockquote {
border-left: 3px solid var(--accent);
padding-left: 12px;
margin: 8px 0;
color: var(--text-secondary);
}
.message.assistant table {
border-collapse: collapse;
width: 100%;
margin: 8px 0;
}
.message.assistant th, .message.assistant td {
border: 1px solid var(--border);
padding: 6px 10px;
text-align: left;
font-size: 13px;
}
.message.assistant th { background: var(--bg-tertiary); }
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 12px;
background: var(--bg-tertiary);
border-radius: 8px 8px 0 0;
font-size: 12px;
color: var(--text-secondary);
margin-bottom: -8px;
margin-top: 8px;
border: 1px solid var(--border);
border-bottom: none;
}
.code-header + pre { border-top-left-radius: 0; border-top-right-radius: 0; }
.copy-btn {
background: var(--accent-dim);
border: none;
color: var(--accent);
padding: 3px 10px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
}
.copy-btn:hover { background: var(--accent); color: white; }
.thinking-indicator {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-secondary);
font-size: 13px;
padding: 4px 0;
}
.thinking-dots span {
display: inline-block;
width: 6px; height: 6px;
background: var(--accent);
border-radius: 50%;
animation: bounce 1.4s infinite;
}
.thinking-dots span:nth-child(2) { animation-delay: 0.2s; }
.thinking-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce { 0%, 80%, 100% { transform: scale(0.6); } 40% { transform: scale(1); } }
/* Chat Input */
.chat-input-area {
padding: 8px 12px 16px;
background: var(--bg-secondary);
border-top: 1px solid var(--border);
}
.mode-selector {
display: flex;
gap: 4px;
margin-bottom: 8px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.mode-selector::-webkit-scrollbar { display: none; }
.mode-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: all var(--transition);
}
.mode-btn.active {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.mode-btn:hover:not(.active) {
background: var(--accent-dim);
border-color: var(--accent);
color: var(--accent);
}
.input-row {
display: flex;
gap: 8px;
align-items: flex-end;
}
#message-input {
flex: 1;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 12px 16px;
border-radius: 24px;
font-size: 15px;
resize: none;
outline: none;
max-height: 120px;
min-height: 44px;
line-height: 1.4;
transition: border-color var(--transition);
font-family: inherit;
}
#message-input:focus { border-color: var(--accent); }
.send-btn, .stop-btn {
width: 44px;
height: 44px;
border-radius: 50%;
border: none;
background: var(--accent);
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition);
flex-shrink: 0;
}
.send-btn:hover { background: var(--accent-hover); }
.send-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.stop-btn { background: var(--danger); }
.stop-btn:hover { background: #ff6b7a; }
/* Sidebar */
.sidebar {
position: fixed;
left: -300px;
top: 0;
bottom: 0;
width: 280px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
z-index: 100;
display: flex;
flex-direction: column;
transition: left 0.3s ease;
}
.sidebar.open { left: 0; }
.sidebar-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--sidebar-overlay-bg);
z-index: 99;
display: none;
}
.sidebar-overlay.active { display: block; }
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--border);
}
.sidebar-header h3 { font-size: 16px; }
.conversation-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.conv-item {
padding: 12px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background var(--transition);
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2px;
}
.conv-item:hover { background: var(--accent-dim); }
.conv-item.active { background: var(--accent-dim); border: 1px solid var(--accent); }
.conv-title { font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
.conv-delete {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 16px;
padding: 4px 8px;
border-radius: 4px;
display: none;
}
.conv-item:hover .conv-delete { display: block; }
.conv-delete:hover { color: var(--danger); background: rgba(255,71,87,0.1); }
.sidebar-footer { padding: 12px; border-top: 1px solid var(--border); }
/* Settings Screen */
.settings-container {
height: 100%;
display: flex;
flex-direction: column;
}
.settings-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
}
.settings-header h2 { font-size: 18px; }
.settings-body {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.settings-section {
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border);
}
.settings-section:last-child { border-bottom: none; }
.settings-section h3 {
font-size: 14px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 16px;
}
.settings-section .btn-secondary,
.settings-section .btn-danger { margin-top: 8px; }
.about-text {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 4px;
}
/* Scrollbar */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
.theme-toggle-btn {
background: none;
border: none;
color: var(--text-primary);
font-size: 20px;
cursor: pointer;
padding: 8px;
border-radius: 8px;
transition: background var(--transition);
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
min-height: 40px;
}
.theme-toggle-btn:hover { background: var(--accent-dim); }
.changelog-list {
list-style: none;
padding: 0;
}
.changelog-list li {
padding: 10px 0;
border-bottom: 1px solid var(--border);
font-size: 13px;
line-height: 1.5;
}
.changelog-list li:last-child { border-bottom: none; }
.changelog-version {
font-weight: 700;
color: var(--accent);
font-size: 14px;
}
.changelog-date {
font-size: 11px;
color: var(--text-muted);
margin-left: 8px;
}
.changelog-list li ul {
margin: 4px 0 0 16px;
padding: 0;
list-style: disc;
}
.changelog-list li ul li {
border-bottom: none;
padding: 2px 0;
color: var(--text-secondary);
font-size: 12px;
}
/* Responsive */
@media (max-width: 480px) {
.message { max-width: 92%; }
.setup-container { gap: 24px; }
.mode-cards { gap: 8px; }
.mode-card { padding: 10px 8px; }
}

241
www/index.html Normal file
View File

@@ -0,0 +1,241 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#1a1a2e">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<title>Z.AI Chat</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div id="app">
<div id="setup-screen" class="screen active">
<div class="setup-container">
<div class="logo-area">
<div class="logo-icon">Z</div>
<h1>Z.AI Chat</h1>
<p class="subtitle">GLM Coding Plan &middot; Powered by GLM-5.1</p>
</div>
<div class="setup-form">
<div class="input-group">
<label for="api-token">Coding Plan Token</label>
<input type="password" id="api-token" placeholder="Enter your Z.AI coding plan API key" autocomplete="off">
<span class="input-hint">Get your key at <a href="https://z.ai/manage-apikey/apikey-list" target="_blank">z.ai/apikeys</a> &middot; <a href="https://z.ai/subscribe" target="_blank">Subscribe</a></span>
</div>
<div class="input-group">
<label for="base-url">Endpoint</label>
<select id="base-url">
<option value="https://api.z.ai/api/coding/paas/v4" selected>Coding Plan (api.z.ai/coding)</option>
<option value="https://api.z.ai/api/paas/v4">General API (api.z.ai)</option>
<option value="https://open.bigmodel.cn/api/paas/v4">China (bigmodel.cn)</option>
</select>
</div>
<button id="connect-btn" class="btn-primary">
<span class="btn-text">Connect</span>
<span class="btn-loader" style="display:none"></span>
</button>
<div id="setup-error" class="error-msg" style="display:none"></div>
</div>
<div class="setup-modes">
<h3>Modes Available</h3>
<div class="mode-cards">
<div class="mode-card">
<span class="mode-icon">&#128172;</span>
<span class="mode-name">Chat</span>
<span class="mode-desc">General conversation</span>
</div>
<div class="mode-card">
<span class="mode-icon">&#128187;</span>
<span class="mode-name">Coding</span>
<span class="mode-desc">Code assistant</span>
</div>
<div class="mode-card">
<span class="mode-icon">&#128161;</span>
<span class="mode-name">Brainstorm</span>
<span class="mode-desc">Ideation partner</span>
</div>
<div class="mode-card">
<span class="mode-icon">&#129302;</span>
<span class="mode-name">Agentic</span>
<span class="mode-desc">Autonomous coding</span>
</div>
</div>
</div>
</div>
</div>
<div id="chat-screen" class="screen">
<div class="chat-header">
<div class="header-left">
<button id="menu-btn" class="icon-btn">&#9776;</button>
<div class="header-title">
<h2 id="conversation-title">New Chat</h2>
<span id="current-mode-label" class="mode-label">Chat</span>
</div>
</div>
<div class="header-right">
<button id="theme-toggle-header" class="theme-toggle-btn" title="Toggle theme">&#9790;</button>
<button id="new-chat-btn" class="icon-btn" title="New chat">+</button>
<button id="settings-btn" class="icon-btn" title="Settings">&#9881;</button>
</div>
</div>
<div id="sidebar" class="sidebar">
<div class="sidebar-header">
<h3>Conversations</h3>
<button id="sidebar-close" class="icon-btn">&times;</button>
</div>
<div id="conversation-list" class="conversation-list"></div>
<div class="sidebar-footer">
<button id="new-chat-sidebar" class="btn-secondary">+ New Chat</button>
</div>
</div>
<div id="sidebar-overlay" class="sidebar-overlay"></div>
<div id="messages" class="messages"></div>
<div class="chat-input-area">
<div id="mode-selector" class="mode-selector">
<button class="mode-btn active" data-mode="chat">Chat</button>
<button class="mode-btn" data-mode="coding">Coding</button>
<button class="mode-btn" data-mode="brainstorm">Brainstorm</button>
<button class="mode-btn" data-mode="agentic">Agentic</button>
</div>
<div class="input-row">
<textarea id="message-input" placeholder="Type your message..." rows="1"></textarea>
<button id="send-btn" class="send-btn" disabled>
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" fill="currentColor"/></svg>
</button>
<button id="stop-btn" class="stop-btn" style="display:none">
<svg viewBox="0 0 24 24" width="24" height="24"><rect x="6" y="6" width="12" height="12" fill="currentColor" rx="2"/></svg>
</button>
</div>
</div>
</div>
<div id="settings-screen" class="screen">
<div class="settings-container">
<div class="settings-header">
<button id="settings-back" class="icon-btn">&larr;</button>
<h2>Settings</h2>
</div>
<div class="settings-body">
<div class="settings-section">
<h3>API Configuration</h3>
<div class="input-group">
<label for="settings-token">API Token</label>
<input type="password" id="settings-token" placeholder="Your API token">
</div>
<div class="input-group">
<label for="settings-url">Base URL</label>
<input type="text" id="settings-url" placeholder="https://api.z.ai/api/coding/paas/v4">
</div>
</div>
<div class="settings-section">
<h3>Model Settings</h3>
<div class="input-group">
<label for="settings-model">Model</label>
<select id="settings-model">
<option value="glm-5.1" selected>GLM-5.1 (Flagship)</option>
<option value="glm-5-turbo">GLM-5 Turbo</option>
<option value="glm-4.7">GLM-4.7</option>
<option value="glm-4.5-air">GLM-4.5 Air (Fast)</option>
<option value="glm-5v-turbo">GLM-5V Turbo (Vision)</option>
</select>
</div>
<div class="input-group">
<label>Temperature: <span id="temp-value">0.7</span></label>
<input type="range" id="settings-temp" min="0" max="1" step="0.1" value="0.7">
</div>
<div class="input-group">
<label>Max Tokens: <span id="tokens-value">4096</span></label>
<input type="range" id="settings-tokens" min="256" max="16384" step="256" value="4096">
</div>
<div class="input-group toggle-group">
<label>Web Search</label>
<label class="toggle">
<input type="checkbox" id="settings-websearch">
<span class="toggle-slider"></span>
</label>
</div>
<div class="input-group toggle-group">
<label>Streaming</label>
<label class="toggle">
<input type="checkbox" id="settings-streaming" checked>
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="settings-section">
<h3>Appearance</h3>
<div class="input-group toggle-group">
<label>Dark Mode</label>
<label class="toggle">
<input type="checkbox" id="settings-darkmode" checked>
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="settings-section">
<h3>Data</h3>
<button id="export-btn" class="btn-secondary">Export Conversations</button>
<button id="clear-btn" class="btn-danger">Clear All Data</button>
</div>
<div class="settings-section">
<h3>About</h3>
<p class="about-text">Z.AI Chat v1.2.0</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.0</span>
<span class="changelog-date">2026-05-19</span>
<ul>
<li>Added light mode / dark mode toggle</li>
<li>Theme persists across sessions</li>
<li>Theme toggle button in chat header</li>
<li>Theme setting in Settings screen</li>
<li>Added changelog section to Settings</li>
<li>Optimized light theme color palette</li>
</ul>
</li>
<li>
<span class="changelog-version">v1.1.0</span>
<span class="changelog-date">2026-05-19</span>
<ul>
<li>Z.AI Coding Plan endpoint support</li>
<li>Fixed API base URL to use coding plan endpoint</li>
<li>Updated model list (GLM-5.1, GLM-5 Turbo, GLM-4.7, GLM-4.5 Air)</li>
</ul>
</li>
<li>
<span class="changelog-version">v1.0.0</span>
<span class="changelog-date">2026-05-19</span>
<ul>
<li>Initial release</li>
<li>Chat, Coding, Brainstorm, Agentic modes</li>
<li>Streaming SSE responses</li>
<li>Markdown rendering with syntax highlighting</li>
<li>Conversation management with sidebar</li>
<li>Settings with model, temperature, tokens controls</li>
<li>Web search integration</li>
<li>Export conversations to JSON</li>
<li>Android 15/16 support (targetSdk 36)</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script src="js/marked.min.js"></script>
<script src="js/highlight.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>

658
www/js/app.js Normal file
View File

@@ -0,0 +1,658 @@
(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 MODE_EMOJIS = { chat: '\u{1F4AC}', coding: '\u{1F4BB}', brainstorm: '\u{1F4A1}', agentic: '\u{1F916}' };
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
};
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';
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 + '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() {
if (!state.activeConversationId) return null;
return state.conversations.find(function(c) { return c.id === state.activeConversationId; });
}
function newConversation() {
var conv = {
id: genId(),
title: 'New Chat',
mode: state.currentMode,
messages: [],
createdAt: Date.now()
};
state.conversations.unshift(conv);
state.activeConversationId = conv.id;
saveState();
renderConversationList();
renderMessages();
updateHeader();
}
function switchConversation(id) {
state.activeConversationId = id;
var conv = getConversation();
if (conv) {
state.currentMode = conv.mode || 'chat';
updateModeSelector();
}
saveState();
renderConversationList();
renderMessages();
updateHeader();
closeSidebar();
}
function deleteConversation(id) {
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();
}
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' : '');
div.innerHTML = '<span class="conv-title">' + escapeHtml(conv.title) + '</span>' +
'<button class="conv-delete" data-id="' + conv.id + '">&times;</button>';
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, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
}
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 = '<span>' + escapeHtml(lang) + '</span><button class="copy-btn">Copy</button>';
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 = '<div class="message system">Start a conversation with Z.AI</div>';
return;
}
conv.messages.forEach(function(msg) {
appendMessage(msg.role, msg.content, container, false);
});
container.scrollTop = container.scrollHeight;
}
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 = '<div class="thinking-indicator"><div class="thinking-dots"><span></span><span></span><span></span></div> Thinking...</div>';
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 });
input.value = '';
autoResize(input);
updateSendButton();
appendMessage('user', text);
state.isGenerating = true;
updateSendButton();
showThinking();
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') {
apiMessages.push({ role: m.role, content: m.content });
}
});
var 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();
var responseDiv = appendMessage('assistant', '');
if (state.streaming) {
await streamResponse(requestBody, responseDiv, conv);
} else {
var result = await apiRequest(requestBody);
var content = result.choices[0].message.content;
updateStreamingMessage(responseDiv, content);
conv.messages.push({ role: 'assistant', content: content });
}
} catch(err) {
removeThinking();
if (err.name !== 'AbortError') {
appendMessage('system', 'Error: ' + (err.message || 'Request failed'));
}
} finally {
state.isGenerating = false;
state.abortController = null;
updateSendButton();
saveState();
}
}
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();
}
async function streamResponse(body, responseDiv, conv) {
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 = '';
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;
updateStreamingMessage(responseDiv, fullContent);
}
} catch(e) {}
}
}
conv.messages.push({ role: 'assistant', content: fullContent });
}
function stopGeneration() {
if (state.abortController) {
state.abortController.abort();
}
}
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' ? '&#9788;' : '&#9790;';
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');
}
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();
$('#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();
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');
});
$('#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();
}
});
updateModeSelector();
updateSendButton();
applyTheme(state.theme);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

1213
www/js/highlight.min.js vendored Normal file

File diff suppressed because one or more lines are too long

69
www/js/marked.min.js vendored Normal file

File diff suppressed because one or more lines are too long