AG X v2.0.3 - Antigravity fork with multi-provider support
Features: - Welcome screen on first run (provider choice before LS starts) - 15+ AI providers (Google Gemini, OpenAI, Anthropic, DeepSeek, Ollama, etc.) - Provider config syncs to endpoints.json for translation proxy - Built-in Node.js translation proxy for non-native backends - Auto-update support, tray integration, URI scheme handler
This commit is contained in:
696
dist/provider/settings.html
vendored
Normal file
696
dist/provider/settings.html
vendored
Normal file
@@ -0,0 +1,696 @@
|
||||
<!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(() => {
|
||||
const currentWindow = require('electron').remote?.getCurrentWindow();
|
||||
if (currentWindow) currentWindow.close();
|
||||
// Fallback: close via ipcRenderer
|
||||
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>
|
||||
Reference in New Issue
Block a user