| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>OpenCode AI Agent</title> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #fff; height: 100vh; display: flex; flex-direction: column; } |
| .header { background: #16213e; padding: 15px 20px; border-bottom: 1px solid #0f3460; } |
| .header h1 { font-size: 1.4em; color: #e94560; } |
| .header p { font-size: 0.85em; color: #888; margin-top: 4px; } |
| .model-select { display: flex; align-items: center; gap: 10px; margin-top: 10px; } |
| .model-select label { font-size: 0.9em; color: #aaa; } |
| .model-select select { background: #0f3460; color: #fff; border: 1px solid #e94560; padding: 6px 12px; border-radius: 6px; font-size: 0.9em; } |
| .chat-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; } |
| .message { max-width: 80%; padding: 12px 16px; border-radius: 12px; line-height: 1.5; font-size: 0.95em; white-space: pre-wrap; word-wrap: break-word; } |
| .message.user { align-self: flex-end; background: #e94560; color: #fff; border-bottom-right-radius: 4px; } |
| .message.bot { align-self: flex-start; background: #16213e; color: #ddd; border-bottom-left-radius: 4px; border: 1px solid #0f3460; } |
| .message.error { align-self: flex-start; background: #5c1a1a; color: #ff6b6b; } |
| .input-area { padding: 15px 20px; background: #16213e; border-top: 1px solid #0f3460; display: flex; gap: 10px; } |
| .input-area textarea { flex: 1; background: #0f3460; color: #fff; border: 1px solid #333; border-radius: 8px; padding: 12px; font-size: 0.95em; resize: none; font-family: inherit; } |
| .input-area textarea:focus { outline: none; border-color: #e94560; } |
| .input-area button { background: #e94560; color: #fff; border: none; border-radius: 8px; padding: 12px 24px; font-size: 0.95em; cursor: pointer; font-weight: 600; } |
| .input-area button:hover { background: #c73650; } |
| .input-area button:disabled { background: #555; cursor: not-allowed; } |
| .api-key-area { padding: 10px 20px; background: #0d1117; border-top: 1px solid #333; } |
| .api-key-area input { width: 100%; background: #161b22; color: #c9d1d9; border: 1px solid #30363d; border-radius: 6px; padding: 8px 12px; font-size: 0.85em; font-family: monospace; } |
| .api-key-area input::placeholder { color: #484f58; } |
| .loading { display: inline-block; } |
| .loading::after { content: ''; animation: dots 1.5s infinite; } |
| @keyframes dots { 0%, 20% { content: '.'; } 40% { content: '..'; } 60%, 100% { content: '...'; } } |
| .free-tag { background: #2ea043; color: #fff; font-size: 0.7em; padding: 2px 6px; border-radius: 4px; margin-left: 6px; } |
| </style> |
| </head> |
| <body> |
| <div class="header"> |
| <h1>🤖 OpenCode AI Agent</h1> |
| <p>Chat with AI models powered by OpenCode Zen + Local DeepSeek R1 7B</p> |
| <div class="model-select"> |
| <label>Model:</label> |
| <select id="model-select"> |
| <optgroup label="GPT Models"> |
| <option value="gpt-5.6-sol">GPT 5.6 Sol</option> |
| <option value="gpt-5.6-terra">GPT 5.6 Terra</option> |
| <option value="gpt-5.6-luna">GPT 5.6 Luna</option> |
| <option value="gpt-5.5" selected>GPT 5.5</option> |
| <option value="gpt-5.5-pro">GPT 5.5 Pro</option> |
| <option value="gpt-5.4">GPT 5.4</option> |
| <option value="gpt-5.4-pro">GPT 5.4 Pro</option> |
| <option value="gpt-5.4-mini">GPT 5.4 Mini</option> |
| <option value="gpt-5.4-nano">GPT 5.4 Nano</option> |
| <option value="gpt-5.3-codex">GPT 5.3 Codex</option> |
| <option value="gpt-5.3-codex-spark">GPT 5.3 Codex Spark</option> |
| <option value="gpt-5.2">GPT 5.2</option> |
| <option value="gpt-5.1">GPT 5.1</option> |
| <option value="gpt-5">GPT 5</option> |
| <option value="gpt-5-nano">GPT 5 Nano</option> |
| </optgroup> |
| <optgroup label="Claude Models"> |
| <option value="claude-fable-5">Claude Fable 5</option> |
| <option value="claude-opus-5">Claude Opus 5</option> |
| <option value="claude-opus-4-8">Claude Opus 4.8</option> |
| <option value="claude-opus-4-7">Claude Opus 4.7</option> |
| <option value="claude-opus-4-5">Claude Opus 4.5</option> |
| <option value="claude-sonnet-5">Claude Sonnet 5</option> |
| <option value="claude-sonnet-4-6">Claude Sonnet 4.6</option> |
| <option value="claude-sonnet-4-5">Claude Sonnet 4.5</option> |
| <option value="claude-haiku-4-5">Claude Haiku 4.5</option> |
| </optgroup> |
| <optgroup label="Gemini Models"> |
| <option value="gemini-3.6-flash">Gemini 3.6 Flash</option> |
| <option value="gemini-3.5-flash">Gemini 3.5 Flash</option> |
| <option value="gemini-3.5-flash-lite">Gemini 3.5 Flash Lite</option> |
| <option value="gemini-3.1-pro">Gemini 3.1 Pro</option> |
| <option value="gemini-3-flash">Gemini 3 Flash</option> |
| </optgroup> |
| <optgroup label="Other Models"> |
| <option value="grok-4.5">Grok 4.5</option> |
| <option value="qwen3.7-max">Qwen3.7 Max</option> |
| <option value="qwen3.7-plus">Qwen3.7 Plus</option> |
| <option value="deepseek-v4-pro">DeepSeek V4 Pro</option> |
| <option value="deepseek-v4-flash">DeepSeek V4 Flash</option> |
| <option value="kimi-k2.7-code">Kimi K2.7 Code</option> |
| <option value="minimax-m3">MiniMax M3</option> |
| <option value="glm-5.2">GLM 5.2</option> |
| </optgroup> |
| <optgroup label="Local Models (Your Server)"> |
| <option value="bartowski/Qwen2.5-Coder-3B-Instruct-GGUF">Qwen2.5-Coder-3B (Local) <span class="free-tag">FREE</span></option> |
| </optgroup> |
| <optgroup label="Free Models"> |
| <option value="deepseek-v4-flash-free">DeepSeek V4 Flash Free <span class="free-tag">FREE</span></option> |
| <option value="mimo-v2.5-free">MiMo-V2.5 Free <span class="free-tag">FREE</span></option> |
| <option value="big-pickle">Big Pickle Free <span class="free-tag">FREE</span></option> |
| <option value="laguna-s-2.1-free">Laguna S 2.1 Free <span class="free-tag">FREE</span></option> |
| <option value="ling-3.0-flash-free">Ling-3.0 Flash Free <span class="free-tag">FREE</span></option> |
| <option value="north-mini-code-free">North Mini Code Free <span class="free-tag">FREE</span></option> |
| <option value="nemotron-3-ultra-free">Nemotron 3 Ultra Free <span class="free-tag">FREE</span></option> |
| </optgroup> |
| </select> |
| </div> |
| </div> |
| <div class="chat-container" id="chat"></div> |
| <div class="api-key-area"> |
| <input type="password" id="api-key" placeholder="Enter your OpenCode Zen API key (sk-...)" /> |
| </div> |
| <div class="input-area"> |
| <textarea id="user-input" placeholder="Type your message..." rows="2"></textarea> |
| <button id="send-btn" onclick="sendMessage()">Send</button> |
| </div> |
|
|
| <script> |
| const chatEl = document.getElementById('chat'); |
| const inputEl = document.getElementById('user-input'); |
| const sendBtn = document.getElementById('send-btn'); |
| const apiKeyEl = document.getElementById('api-key'); |
| const modelSelect = document.getElementById('model-select'); |
| let history = []; |
| |
| |
| const savedKey = localStorage.getItem('opencode_zen_key'); |
| if (savedKey) apiKeyEl.value = savedKey; |
| |
| inputEl.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } |
| }); |
| |
| function addMessage(role, content, isError = false) { |
| const div = document.createElement('div'); |
| div.className = `message ${role}${isError ? ' error' : ''}`; |
| div.textContent = content; |
| chatEl.appendChild(div); |
| chatEl.scrollTop = chatEl.scrollHeight; |
| } |
| |
| function addLoading() { |
| const div = document.createElement('div'); |
| div.className = 'message bot'; |
| div.id = 'loading'; |
| div.innerHTML = '<span class="loading">Thinking</span>'; |
| chatEl.appendChild(div); |
| chatEl.scrollTop = chatEl.scrollHeight; |
| } |
| |
| function removeLoading() { |
| const el = document.getElementById('loading'); |
| if (el) el.remove(); |
| } |
| |
| const LOCAL_API = 'https://abbasyk60-ai-telegram-bot.hf.space/v1/chat/completions'; |
| const LOCAL_MODEL = 'bartowski/Qwen2.5-Coder-3B-Instruct-GGUF'; |
| |
| function getEndpoint(modelId) { |
| if (modelId === LOCAL_MODEL) return 'local'; |
| const openaiPrefixes = ['gpt-', 'deepseek-', 'grok-', 'minimax-', 'glm-', 'kimi-', 'big-pickle', 'mimo-', 'laguna-', 'ling-', 'north-', 'nemotron-']; |
| if (openaiPrefixes.some(p => modelId.startsWith(p) || modelId.includes(p))) { |
| return 'https://opencode.ai/zen/v1/chat/completions'; |
| } |
| if (modelId.startsWith('claude-') || modelId.startsWith('qwen3')) { |
| return 'https://opencode.ai/zen/v1/messages'; |
| } |
| if (modelId.startsWith('gemini-')) { |
| return 'https://opencode.ai/zen/v1/models/' + modelId; |
| } |
| return 'https://opencode.ai/zen/v1/chat/completions'; |
| } |
| |
| async function sendMessage() { |
| const message = inputEl.value.trim(); |
| const apiKey = apiKeyEl.value.trim(); |
| const modelId = modelSelect.value; |
| const isLocal = modelId === LOCAL_MODEL; |
| |
| if (!isLocal && !apiKey) { addMessage('error', 'Please enter your OpenCode Zen API key first. (Not needed for local model)'); return; } |
| if (!message) return; |
| |
| if (!isLocal) localStorage.setItem('opencode_zen_key', apiKey); |
| |
| addMessage('user', message); |
| inputEl.value = ''; |
| sendBtn.disabled = true; |
| addLoading(); |
| |
| const endpoint = getEndpoint(modelId); |
| const messages = [...history, { role: 'user', content: message }]; |
| |
| try { |
| let response, data; |
| |
| if (isLocal) { |
| response = await fetch(LOCAL_API, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ model: LOCAL_MODEL, messages: messages, max_tokens: 4096 }) |
| }); |
| } else if (endpoint.includes('/messages')) { |
| response = await fetch(endpoint, { |
| method: 'POST', |
| headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' }, |
| body: JSON.stringify({ model: modelId, messages: messages, max_tokens: 4096 }) |
| }); |
| } else if (endpoint.includes('/models/')) { |
| response = await fetch(endpoint, { |
| method: 'POST', |
| headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ contents: [{ parts: [{ text: message }] }] }) |
| }); |
| } else { |
| response = await fetch(endpoint, { |
| method: 'POST', |
| headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ model: modelId, messages: messages, max_tokens: 4096 }) |
| }); |
| } |
| |
| removeLoading(); |
| |
| if (!response.ok) { |
| const errText = await response.text(); |
| addMessage('error', `API Error ${response.status}: ${errText.substring(0, 500)}`); |
| sendBtn.disabled = false; |
| return; |
| } |
| |
| data = await response.json(); |
| let botMessage = ''; |
| |
| if (data.choices && data.choices[0]) { |
| const msg = data.choices[0].message; |
| botMessage = msg.content || msg.reasoning_content || ''; |
| } else if (endpoint.includes('/messages') && data.content) { |
| botMessage = data.content.map(c => c.text || '').join(''); |
| } else if (data.candidates && data.candidates[0]) { |
| botMessage = data.candidates[0].content.parts.map(p => p.text).join(''); |
| } else { |
| botMessage = JSON.stringify(data, null, 2).substring(0, 2000); |
| } |
| |
| if (!botMessage) botMessage = '(empty response - try again)'; |
| |
| addMessage('bot', botMessage); |
| history.push({ role: 'user', content: message }); |
| history.push({ role: 'assistant', content: botMessage }); |
| |
| |
| if (history.length > 20) history = history.slice(-20); |
| |
| } catch (err) { |
| removeLoading(); |
| addMessage('error', `Error: ${err.message}`); |
| } |
| |
| sendBtn.disabled = false; |
| inputEl.focus(); |
| } |
| </script> |
| </body> |
| </html> |
|
|