Files
ag-x/dist/provider/settings.html
admin f7378eceb0 v2.0.5: Fix E2E flow - proxy, welcome screen, provider sync
Critical fixes:
- Translation proxy now uses system Node.js (not Electron binary)
- Removed duplicate proxy start causing port conflicts
- Added port availability check before spawning proxy
- Fixed welcome:choice double resolve()
- Fixed settings.html close using deprecated remote
- Fixed translationProxy /v1 for openai-compat backends
- Proxy no longer detached/unref - properly tracked as child
- SingletonLock cleanup on startup

Verified E2E:
- Welcome screen on first run ✓
- Provider selection works ✓
- Settings save + sync ✓
- Translation proxy starts correctly ✓
- LS connects to proxy ✓
- --ag-reset works ✓
2026-05-23 12:14:04 +04:00

695 lines
26 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Provider Settings</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e; color: #e0e0e0;
min-height: 100vh; padding: 0;
}
.header {
background: linear-gradient(135deg, #16213e 0%, #0f3460 100%);
padding: 20px 32px; border-bottom: 1px solid #2a2a4a;
display: flex; align-items: center; justify-content: space-between;
-webkit-app-region: drag;
}
.header h1 { font-size: 20px; font-weight: 600; color: #fff; }
.header .subtitle { font-size: 12px; color: #8892b0; margin-top: 2px; }
.header-badge {
background: #e94560; color: #fff; font-size: 10px;
padding: 2px 8px; border-radius: 10px; font-weight: 600;
}
.container { padding: 24px 32px; max-width: 900px; margin: 0 auto; }
/* Category headings */
.category-title {
font-size: 11px; font-weight: 700; color: #8892b0;
text-transform: uppercase; letter-spacing: 1px;
margin: 20px 0 10px 0; padding-bottom: 4px;
border-bottom: 1px solid #2a2a4a;
}
.provider-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px; margin-bottom: 20px;
}
.provider-card {
background: #16213e; border: 2px solid #2a2a4a; border-radius: 12px;
padding: 14px; cursor: pointer; transition: all 0.2s;
position: relative;
}
.provider-card:hover { border-color: #0f3460; transform: translateY(-1px); }
.provider-card.active { border-color: #e94560; background: #1a1a3e; }
.provider-card.active::after {
content: '✓'; position: absolute; top: 10px; right: 12px;
color: #e94560; font-size: 18px; font-weight: bold;
}
.provider-icon { font-size: 26px; margin-bottom: 6px; }
.provider-name { font-size: 13px; font-weight: 600; color: #fff; }
.provider-desc { font-size: 10px; color: #8892b0; margin-top: 3px; line-height: 1.3; }
.section {
background: #16213e; border: 1px solid #2a2a4a; border-radius: 12px;
padding: 24px; margin-bottom: 20px;
}
.section-title {
font-size: 15px; font-weight: 600; color: #fff;
margin-bottom: 16px; display: flex; align-items: center; gap: 8px;
}
.section-title .icon { font-size: 18px; }
.form-group { margin-bottom: 16px; }
.form-group label {
display: block; font-size: 12px; font-weight: 500;
color: #8892b0; margin-bottom: 6px; text-transform: uppercase;
letter-spacing: 0.5px;
}
.form-group input, .form-group select {
width: 100%; padding: 10px 14px; background: #0f1a2e;
border: 1px solid #2a2a4a; border-radius: 8px; color: #e0e0e0;
font-size: 14px; outline: none; transition: border-color 0.2s;
}
.form-group input:focus, .form-group select:focus {
border-color: #e94560;
}
.form-group input::placeholder { color: #555; }
.form-group select option { background: #16213e; color: #e0e0e0; }
.api-key-wrapper { position: relative; }
.api-key-wrapper input { padding-right: 40px; }
.toggle-visibility {
position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
background: none; border: none; color: #8892b0; cursor: pointer;
font-size: 14px;
}
.btn-row { display: flex; gap: 12px; margin-top: 24px; }
.btn {
padding: 10px 24px; border-radius: 8px; font-size: 14px;
font-weight: 600; cursor: pointer; border: none; transition: all 0.2s;
}
.btn-primary { background: #e94560; color: #fff; }
.btn-primary:hover { background: #c73650; }
.btn-secondary { background: #2a2a4a; color: #e0e0e0; }
.btn-secondary:hover { background: #3a3a5a; }
.btn-danger { background: #4a1a1a; color: #e94560; }
.btn-danger:hover { background: #5a2020; }
.status-bar {
background: #0f1a2e; border-top: 1px solid #2a2a4a;
padding: 10px 32px; display: flex; align-items: center;
justify-content: space-between; font-size: 12px; color: #8892b0;
}
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
.status-dot.connected { background: #4caf50; }
.status-dot.disconnected { background: #e94560; }
.status-dot.unknown { background: #ffa726; }
.toast {
position: fixed; bottom: 60px; left: 50%; transform: translateX(-50%);
background: #4caf50; color: #fff; padding: 10px 24px; border-radius: 8px;
font-size: 14px; font-weight: 500; opacity: 0; transition: opacity 0.3s;
pointer-events: none; z-index: 100;
}
.toast.show { opacity: 1; }
.toast.error { background: #e94560; }
.help-text { font-size: 11px; color: #666; margin-top: 4px; }
.models-container {
display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;
}
.model-chip {
padding: 4px 12px; background: #0f1a2e; border: 1px solid #2a2a4a;
border-radius: 16px; font-size: 12px; color: #8892b0; cursor: pointer;
transition: all 0.2s;
}
.model-chip:hover { border-color: #e94560; color: #e0e0e0; }
.model-chip.selected { background: #e94560; color: #fff; border-color: #e94560; }
.test-result {
margin-top: 12px; padding: 12px; border-radius: 8px;
font-size: 13px; font-family: monospace; display: none;
}
.test-result.success { display: block; background: #1a2e1a; border: 1px solid #2e4a2e; color: #4caf50; }
.test-result.error { display: block; background: #2e1a1a; border: 1px solid #4a2e2e; color: #e94560; }
.tab-bar { display: flex; gap: 0; margin-bottom: 20px; }
.tab {
padding: 8px 20px; background: transparent; border: none;
border-bottom: 2px solid transparent; color: #8892b0;
cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.tab:hover { color: #e0e0e0; }
.tab.active { color: #e94560; border-bottom-color: #e94560; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.badge {
display: inline-block; padding: 2px 8px; border-radius: 4px;
font-size: 10px; font-weight: 600; margin-left: 6px;
}
.badge-native { background: #1a3a1a; color: #4caf50; }
.badge-proxy { background: #3a3a1a; color: #ffa726; }
.badge-cc { background: #3a1a3a; color: #ce93d8; }
</style>
</head>
<body>
<div class="header">
<div>
<h1>AI Provider Settings</h1>
<div class="subtitle">Choose and configure your AI provider — AG X supports 20+ providers</div>
</div>
<span class="header-badge">v3.7</span>
</div>
<div class="container">
<div class="tab-bar">
<button class="tab active" data-tab="providers">Provider</button>
<button class="tab" data-tab="advanced">Advanced</button>
<button class="tab" data-tab="about">About</button>
</div>
<!-- Providers Tab -->
<div class="tab-content active" id="tab-providers">
<div class="section">
<div class="section-title"><span class="icon"></span> Select AI Provider</div>
<div id="providerGridContainer"></div>
</div>
<div class="section" id="providerConfig">
<div class="section-title"><span class="icon">🔧</span> <span id="configTitle">Provider Configuration</span></div>
<div class="form-group">
<label>API Base URL</label>
<input type="text" id="apiUrl" placeholder="https://api.example.com">
<div class="help-text" id="apiUrlHelp"></div>
</div>
<div class="form-group" id="apiKeyGroup">
<label>API Key</label>
<div class="api-key-wrapper">
<input type="password" id="apiKey" placeholder="Enter your API key">
<button class="toggle-visibility" onclick="toggleKeyVisibility()">👁</button>
</div>
<div class="help-text" id="apiKeyHelp"></div>
</div>
<div class="form-group">
<label>Model</label>
<input type="text" id="modelInput" placeholder="Enter model name or select below">
<div class="models-container" id="modelChips"></div>
</div>
<div class="form-group">
<button class="btn btn-secondary" onclick="testConnection()">Test Connection</button>
<div class="test-result" id="testResult"></div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="saveSettings()">Save & Apply</button>
<button class="btn btn-secondary" onclick="resetToDefaults()">Reset Defaults</button>
</div>
</div>
<!-- Advanced Tab -->
<div class="tab-content" id="tab-advanced">
<div class="section">
<div class="section-title"><span class="icon">🛠️</span> Advanced Settings</div>
<div class="form-group">
<label>Proxy Port</label>
<input type="number" id="proxyPort" value="9876" min="1024" max="65535">
<div class="help-text">Port for the local API proxy (default: 9876). Restart required.</div>
</div>
<div class="form-group">
<label>Translate Proxy Path</label>
<div style="padding:10px;background:#0f1a2e;border-radius:8px;font-size:12px;color:#4caf50;">✅ Built-in Node.js translation proxy — no external tools needed</div>
<div class="help-text">Translation proxy is built into AG X for all provider types.</div>
</div>
<div class="form-group">
<label>Command Code Version</label>
<input type="text" id="ccVersion" value="0.26.8">
<div class="help-text">x-command-code-version header value (for Command Code provider only).</div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="saveAdvanced()">Save Advanced</button>
</div>
</div>
<!-- About Tab -->
<div class="tab-content" id="tab-about">
<div class="section">
<div class="section-title"><span class="icon"></span> About Provider System</div>
<p style="color: #8892b0; line-height: 1.6; font-size: 13px;">
AG X supports multiple AI providers through a modular provider system.<br><br>
<strong style="color:#e0e0e0;">Provider Types:</strong><br>
<strong>Native</strong> — Direct connection to the provider API (Google Gemini, OpenAI)<br>
<strong>OpenAI-Compatible</strong> — Any provider with a Chat Completions endpoint<br>
<strong>Anthropic</strong> — Claude models via the Messages API<br>
<strong>Command Code</strong> — 20+ models via Command Code's /alpha/generate API<br><br>
<strong style="color:#e0e0e0;">Translation:</strong><br>
Simple providers use a built-in lightweight proxy. Complex providers (Anthropic, Command Code)
uses the <strong>built-in Node.js proxy</strong>
for full Responses API ↔ provider API translation.<br><br>
<strong style="color:#e0e0e0;">Supported Providers:</strong><br>
Google Gemini, OpenAI, Anthropic, Z.AI, OpenCode (Zen/Go), Crof.ai, NVIDIA NIM,
Kilo.ai, Command Code, OpenRouter, OpenAdapter, DeepSeek, Ollama, Together AI, Groq, and Custom.
</p>
</div>
</div>
</div>
<div class="status-bar">
<div>
<span class="status-dot unknown" id="statusDot"></span>
<span id="statusText">Loading provider configuration…</span>
</div>
<div id="backendBadge" style="font-size:11px; color:#555;"></div>
</div>
<div class="toast" id="toast"></div>
<script>
const { ipcRenderer } = require('electron');
// Provider definitions — mirrors providerService.js
const PROVIDERS = {
google_gemini: {
name: 'Google Gemini (OAuth)', icon: '🔮',
desc: 'Built-in Gemini — zero config',
backendType: 'gemini-native', requiresApiKey: false,
apiUrl: 'https://daily-cloudcode-pa.sandbox.googleapis.com',
models: ['gemini-2.5-pro','gemini-2.5-flash','gemini-2.0-flash','gemini-3-flash-preview','gemini-3-pro-preview'],
defaultModel: 'gemini-2.5-pro',
category: 'Google',
},
openai: {
name: 'OpenAI', icon: '🟢',
desc: 'GPT models via Responses API',
backendType: 'native', requiresApiKey: true, apiKeyHint: 'sk-...',
apiUrl: 'https://api.openai.com/v1',
models: ['gpt-4o','gpt-4o-mini','o1','o1-mini','o3','o3-mini'],
defaultModel: 'gpt-4o',
category: 'Direct',
},
anthropic: {
name: 'Anthropic', icon: '🟣',
desc: 'Claude models via Messages API',
backendType: 'anthropic', requiresApiKey: true, apiKeyHint: 'sk-ant-...',
apiUrl: 'https://api.anthropic.com',
models: ['claude-sonnet-4-20250514','claude-3-5-sonnet-20241022','claude-3-5-haiku-20241022','claude-3-opus-20240229'],
defaultModel: 'claude-sonnet-4-20250514',
category: 'Direct',
},
z_ai: {
name: 'Z.AI Coding', icon: '🅩',
desc: 'GLM & Z models via Z.AI',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'Z.AI Key',
apiUrl: 'https://api.z.ai/api/coding/paas/v4',
models: ['glm-5.1','glm-4.7','GLM-4-Plus','GLM-4-Long','GLM-4-Flash','GLM-4-FlashX','GLM-Z1-Flash'],
defaultModel: 'glm-5.1',
category: 'OpenAI-Compatible',
},
opencode_zen: {
name: 'OpenCode Zen', icon: '🧘',
desc: 'Multi-model via OpenCode Zen',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'OpenCode Key',
apiUrl: 'https://opencode.ai/zen/v1',
models: ['glm-5.1','glm-5','kimi-k2.5','kimi-k2.6','minimax-m2.7','minimax-m2.5','deepseek-v4-flash-free','qwen3.6-plus','big-pickle'],
defaultModel: 'glm-5.1',
category: 'OpenAI-Compatible',
},
opencode_go: {
name: 'OpenCode Go', icon: '🚀',
desc: 'Multi-model via OpenCode Go',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'OpenCode Key',
apiUrl: 'https://opencode.ai/zen/go/v1',
models: ['glm-5.1','glm-5','kimi-k2.5','kimi-k2.6','mimo-v2.5','minimax-m2.7','qwen3.6-plus','deepseek-v4-pro','deepseek-v4-flash'],
defaultModel: 'glm-5.1',
category: 'OpenAI-Compatible',
},
opencode_zen_anthropic: {
name: 'OpenCode Zen (Anthropic)', icon: '🧘',
desc: 'Claude models via OpenCode Zen',
backendType: 'anthropic', requiresApiKey: true, apiKeyHint: 'OpenCode Key',
apiUrl: 'https://opencode.ai/zen/v1',
models: ['claude-opus-4-7','claude-opus-4-6','claude-sonnet-4-6','claude-sonnet-4-5','claude-sonnet-4','claude-haiku-4-5'],
defaultModel: 'claude-sonnet-4-6',
category: 'OpenAI-Compatible',
},
opencode_go_anthropic: {
name: 'OpenCode Go (Anthropic)', icon: '🚀',
desc: 'Claude models via OpenCode Go',
backendType: 'anthropic', requiresApiKey: true, apiKeyHint: 'OpenCode Key',
apiUrl: 'https://opencode.ai/zen/go/v1',
models: ['minimax-m2.7','minimax-m2.5'],
defaultModel: 'minimax-m2.7',
category: 'OpenAI-Compatible',
},
crof_ai: {
name: 'Crof.ai', icon: '🌐',
desc: 'Models via Crof.ai',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'Crof.ai Key',
apiUrl: 'https://crof.ai/v1',
models: [],
defaultModel: '',
category: 'OpenAI-Compatible',
},
nvidia_nim: {
name: 'NVIDIA NIM', icon: '💚',
desc: 'NVIDIA accelerated inference',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'nvapi-...',
apiUrl: 'https://integrate.api.nvidia.com/v1',
models: [],
defaultModel: '',
category: 'OpenAI-Compatible',
},
kilo_ai: {
name: 'Kilo.ai', icon: '⚖️',
desc: 'Multi-provider gateway',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'Kilo.ai Key',
apiUrl: 'https://api.kilo.ai/api/gateway',
models: [],
defaultModel: '',
category: 'OpenAI-Compatible',
},
command_code: {
name: 'Command Code', icon: '⌘',
desc: '20+ models via CC API',
backendType: 'command-code', requiresApiKey: true, apiKeyHint: 'CC API Key',
apiUrl: 'https://api.commandcode.ai',
models: [
'deepseek/deepseek-v4-flash','deepseek/deepseek-v4-pro',
'anthropic:claude-sonnet-4-6','anthropic:claude-haiku-4-5-20251001',
'anthropic:claude-opus-4-7','anthropic:claude-opus-4-6',
'openai:gpt-5.5','openai:gpt-5.4','openai:gpt-5.4-mini','openai:gpt-5.3-codex',
'moonshotai/Kimi-K2.6','moonshotai/Kimi-K2.5',
'zai-org/GLM-5.1','zai-org/GLM-5',
'MiniMaxAI/MiniMax-M2.7','MiniMaxAI/MiniMax-M2.5',
'Qwen/Qwen3.6-Max-Preview','Qwen/Qwen3.6-Plus',
'stepfun/Step-3.5-Flash','google/gemini-3.1-flash-lite',
],
defaultModel: 'deepseek/deepseek-v4-flash',
category: 'Command Code',
},
openrouter: {
name: 'OpenRouter', icon: '🔀',
desc: 'Hundreds of models',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'sk-or-...',
apiUrl: 'https://openrouter.ai/api/v1',
models: [],
defaultModel: '',
category: 'OpenAI-Compatible',
},
openadapter: {
name: 'OpenAdapter', icon: '🔌',
desc: 'Free/proxy models',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'OA Key',
apiUrl: 'https://api.openadapter.in/v1',
models: ['0G-DeepSeek-V3','0G-DeepSeek-v4-Pro','0G-GLM-5','0G-GLM-5.1','0G-Qwen3.6','0G-Qwen-VL'],
defaultModel: '0G-DeepSeek-v4-Pro',
category: 'OpenAI-Compatible',
},
deepseek: {
name: 'DeepSeek', icon: '🔍',
desc: 'DeepSeek models directly',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'DS Key',
apiUrl: 'https://api.deepseek.com/v1',
models: ['deepseek-chat','deepseek-reasoner'],
defaultModel: 'deepseek-chat',
category: 'OpenAI-Compatible',
},
ollama: {
name: 'Ollama (Local)', icon: '🦙',
desc: 'Run models locally',
backendType: 'openai-compat', requiresApiKey: false,
apiUrl: 'http://127.0.0.1:11434',
models: ['llama3.1','llama3','codellama','mistral','mixtral','deepseek-coder','qwen2.5-coder'],
defaultModel: 'llama3.1',
category: 'Local',
},
together: {
name: 'Together AI', icon: '🤝',
desc: 'Open-source models',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'Together Key',
apiUrl: 'https://api.together.xyz/v1',
models: [],
defaultModel: '',
category: 'OpenAI-Compatible',
},
groq: {
name: 'Groq', icon: '⚡',
desc: 'Ultra-fast inference',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'gsk_...',
apiUrl: 'https://api.groq.com/openai/v1',
models: [],
defaultModel: '',
category: 'OpenAI-Compatible',
},
custom: {
name: 'Custom Provider', icon: '⚙️',
desc: 'Any OpenAI-compat endpoint',
backendType: 'openai-compat', requiresApiKey: false,
apiUrl: '',
models: [],
defaultModel: '',
category: 'Custom',
},
};
// Category order
const CATEGORIES = ['Google', 'Direct', 'OpenAI-Compatible', 'Command Code', 'Local', 'Custom'];
let currentConfig = null;
let selectedProvider = null;
async function init() {
try {
currentConfig = await ipcRenderer.invoke('provider:get-config');
} catch (e) {
console.error('Failed to load config:', e);
currentConfig = { activeProvider: 'google_gemini', providers: {} };
}
selectedProvider = currentConfig.activeProvider;
renderProviderGrid();
selectProvider(selectedProvider);
updateStatus();
}
function renderProviderGrid() {
const container = document.getElementById('providerGridContainer');
container.innerHTML = '';
// Group providers by category
const grouped = {};
for (const [key, prov] of Object.entries(PROVIDERS)) {
const cat = prov.category || 'Other';
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push({ key, ...prov });
}
for (const cat of CATEGORIES) {
if (!grouped[cat]) continue;
const title = document.createElement('div');
title.className = 'category-title';
title.textContent = cat;
container.appendChild(title);
const grid = document.createElement('div');
grid.className = 'provider-grid';
for (const prov of grouped[cat]) {
const card = document.createElement('div');
card.className = 'provider-card' + (prov.key === selectedProvider ? ' active' : '');
card.dataset.provider = prov.key;
card.innerHTML = `
<div class="provider-icon">${prov.icon}</div>
<div class="provider-name">${prov.name}</div>
<div class="provider-desc">${prov.desc}</div>
`;
card.addEventListener('click', () => selectProvider(prov.key));
grid.appendChild(card);
}
container.appendChild(grid);
}
}
function selectProvider(key) {
selectedProvider = key;
// Update active card
document.querySelectorAll('.provider-card').forEach(c => {
c.classList.toggle('active', c.dataset.provider === key);
});
const prov = PROVIDERS[key];
const config = currentConfig?.providers?.[key] || {};
document.getElementById('configTitle').textContent = prov.name + ' Configuration';
document.getElementById('apiUrl').value = config.apiUrl || prov.apiUrl || '';
document.getElementById('apiKey').value = config.apiKey || '';
document.getElementById('modelInput').value = config.model || config.defaultModel || prov.defaultModel || '';
document.getElementById('apiKeyHelp').textContent = prov.apiKeyHint || '';
// Show/hide API key field
const apiKeyGroup = document.getElementById('apiKeyGroup');
apiKeyGroup.style.display = prov.requiresApiKey ? 'block' : 'none';
// Render model chips
const chipsContainer = document.getElementById('modelChips');
const models = prov.models || [];
if (models.length === 0) {
chipsContainer.innerHTML = '<div class="help-text">Type a model name above or use "Fetch Models" after saving</div>';
} else {
const currentModel = document.getElementById('modelInput').value;
chipsContainer.innerHTML = models.map(m =>
`<div class="model-chip${m === currentModel ? ' selected' : ''}" onclick="selectModel('${m}')">${m}</div>`
).join('');
}
// Backend badge
const badge = document.getElementById('backendBadge');
const bt = prov.backendType;
const badges = {
'gemini-native': '<span class="badge badge-native">NATIVE</span>',
'native': '<span class="badge badge-native">NATIVE</span>',
'openai-compat': '<span class="badge badge-proxy">OPENAI-COMPAT</span>',
'anthropic': '<span class="badge badge-proxy">ANTHROPIC</span>',
'command-code': '<span class="badge badge-cc">COMMAND CODE</span>',
};
badge.innerHTML = (badges[bt] || bt) + ' ' + prov.name;
updateStatus();
}
function selectModel(model) {
document.getElementById('modelInput').value = model;
document.querySelectorAll('.model-chip').forEach(c => {
c.classList.toggle('selected', c.textContent === model);
});
}
function toggleKeyVisibility() {
const input = document.getElementById('apiKey');
input.type = input.type === 'password' ? 'text' : 'password';
}
async function saveSettings() {
const key = selectedProvider;
const prov = PROVIDERS[key];
const settings = {
activeProvider: key,
providerConfig: {
apiUrl: document.getElementById('apiUrl').value,
apiKey: document.getElementById('apiKey').value,
model: document.getElementById('modelInput').value,
backendType: prov.backendType,
requiresApiKey: prov.requiresApiKey,
},
};
try {
const result = await ipcRenderer.invoke('provider:save', settings);
currentConfig = await ipcRenderer.invoke('provider:get-config');
if (result && result.needsRestart) {
showToast('✓ Provider configured! Starting AG X...');
updateStatus();
// Auto-close settings window after a brief delay
setTimeout(() => {
// Close via IPC — providerSettings.js handles the actual close
require('electron').ipcRenderer.send('provider:close-settings');
}, 1500);
} else {
showToast('Settings saved! Provider: ' + prov.name);
updateStatus();
}
} catch (e) {
showToast('Error saving: ' + e.message, true);
}
}
async function resetToDefaults() {
try {
await ipcRenderer.invoke('provider:reset');
currentConfig = await ipcRenderer.invoke('provider:get-config');
selectedProvider = currentConfig.activeProvider;
renderProviderGrid();
selectProvider(selectedProvider);
showToast('Reset to defaults');
updateStatus();
} catch (e) {
showToast('Error resetting: ' + e.message, true);
}
}
async function testConnection() {
const resultEl = document.getElementById('testResult');
resultEl.className = 'test-result';
resultEl.style.display = 'block';
resultEl.textContent = 'Testing connection…';
const prov = PROVIDERS[selectedProvider];
try {
const result = await ipcRenderer.invoke('provider:test-connection', {
provider: selectedProvider,
apiUrl: document.getElementById('apiUrl').value,
apiKey: document.getElementById('apiKey').value,
model: document.getElementById('modelInput').value,
backendType: prov.backendType,
});
if (result.success) {
resultEl.className = 'test-result success';
resultEl.textContent = '✓ ' + result.message;
} else {
resultEl.className = 'test-result error';
resultEl.textContent = '✗ ' + result.error;
}
} catch (e) {
resultEl.className = 'test-result error';
resultEl.textContent = '✗ ' + e.message;
}
}
async function saveAdvanced() {
try {
await ipcRenderer.invoke('provider:save-advanced', {
proxyPort: parseInt(document.getElementById('proxyPort').value) || 9876,
translateProxyPath: document.getElementById('translateProxyPath').value,
ccVersion: document.getElementById('ccVersion').value,
});
showToast('Advanced settings saved');
} catch (e) {
showToast('Error: ' + e.message, true);
}
}
function showToast(message, isError = false) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast show' + (isError ? ' error' : '');
setTimeout(() => { toast.className = 'toast'; }, 3000);
}
function updateStatus() {
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
const prov = PROVIDERS[selectedProvider];
if (prov) {
const config = currentConfig?.providers?.[selectedProvider];
if (prov.backendType === 'gemini-native' || prov.backendType === 'native') {
dot.className = 'status-dot connected';
text.textContent = prov.name + ' — Direct connection (no proxy needed)';
} else if (config?.apiKey) {
dot.className = 'status-dot unknown';
text.textContent = prov.name + ' — API key configured, proxy will start on launch';
} else {
dot.className = 'status-dot disconnected';
text.textContent = prov.name + ' — API key required';
}
}
}
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
tab.classList.add('active');
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
});
});
init();
</script>
</body>
</html>