Spaces:
Running
Running
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>🦀 Hermès Agent</title> | |
| <style> | |
| :root { | |
| --primary: #8b5cf6; | |
| --secondary: #ec4899; | |
| --bg: #fafafa; | |
| --surface: #ffffff; | |
| --text: #1f2937; | |
| --border: #e5e7eb; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; } | |
| header { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; } | |
| header h1 { font-size: 20px; font-weight: 700; } | |
| header .badge { background: rgba(255,255,255,0.2); padding: 4px 12px; border-radius: 20px; font-size: 12px; } | |
| .container { max-width: 900px; margin: 0 auto; padding: 24px; width: 100%; flex: 1; display: flex; flex-direction: column; gap: 16px; } | |
| .settings { background: var(--surface); border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); } | |
| .settings h3 { font-size: 13px; color: #9ca3af; margin-bottom: 14px; text-transform: uppercase; letter-spacing: 0.5px; } | |
| .settings-grid { display: grid; grid-template-columns: 1fr 180px; gap: 12px; } | |
| .settings-row { display: flex; gap: 12px; flex-wrap: wrap; } | |
| .settings-row > * { flex: 1; min-width: 200px; } | |
| @media (max-width: 640px) { | |
| .settings-grid { grid-template-columns: 1fr; } | |
| .settings-row > * { min-width: 100%; } | |
| } | |
| input, select, textarea { | |
| width: 100%; padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; outline: none; transition: border-color 0.2s, box-shadow 0.2s; background: var(--surface); | |
| } | |
| input:focus, select:focus, textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(139,92,246,0.1); } | |
| select { cursor: pointer; } | |
| .field-hint { font-size: 11px; color: #9ca3af; margin-top: 5px; } | |
| .field-hint code { background: #f3f4f6; padding: 1px 5px; border-radius: 3px; font-size: 11px; } | |
| .provider-tabs { display: flex; gap: 4px; margin-bottom: 14px; background: #f3f4f6; padding: 4px; border-radius: 10px; } | |
| .provider-tab { flex: 1; padding: 8px 12px; text-align: center; border-radius: 7px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s; color: #6b7280; border: none; background: transparent; } | |
| .provider-tab.active { background: var(--surface); color: var(--primary); box-shadow: 0 1px 3px rgba(0,0,0,0.1); } | |
| .provider-tab:hover:not(.active) { color: var(--text); } | |
| .api-key-section { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 12px; } | |
| .api-key-section label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; } | |
| .chat-container { background: var(--surface); border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); flex: 1; display: flex; flex-direction: column; min-height: 480px; } | |
| .chat-header { padding: 14px 20px; border-bottom: 1px solid var(--border); font-size: 13px; color: #6b7280; display: flex; align-items: center; gap: 8px; } | |
| .chat-header .dot { width: 8px; height: 8px; border-radius: 50%; background: #10b981; } | |
| .chat-messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 14px; } | |
| .msg { max-width: 82%; padding: 11px 15px; border-radius: 14px; line-height: 1.65; font-size: 14px; white-space: pre-wrap; word-break: break-word; } | |
| .msg.user { align-self: flex-end; background: var(--primary); color: white; border-bottom-right-radius: 4px; } | |
| .msg.assistant { align-self: flex-start; background: #f3f4f6; border-bottom-left-radius: 4px; } | |
| .msg.error { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; } | |
| .msg.system { align-self: center; background: transparent; color: #9ca3af; font-size: 13px; text-align: center; max-width: 100%; } | |
| .msg .model-tag { display: inline-block; font-size: 10px; background: rgba(0,0,0,0.06); padding: 1px 6px; border-radius: 4px; margin-bottom: 6px; color: #9ca3af; } | |
| .msg.user .model-tag { background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.8); } | |
| .chat-input-area { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; gap: 10px; } | |
| .chat-input-area textarea { resize: none; flex: 1; height: 48px; } | |
| .send-btn { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; border: none; padding: 0 24px; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 14px; transition: opacity 0.2s; white-space: nowrap; } | |
| .send-btn:hover { opacity: 0.88; } | |
| .send-btn:disabled { opacity: 0.5; cursor: not-allowed; } | |
| .loading { display: none; align-items: center; gap: 8px; color: #6b7280; font-size: 13px; padding: 8px 0; } | |
| .loading.show { display: flex; } | |
| .spinner { width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.8s linear infinite; } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .examples { display: flex; flex-wrap: wrap; gap: 8px; } | |
| .example-chip { background: #f3f4f6; border: 1px solid var(--border); padding: 6px 14px; border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.15s; } | |
| .example-chip:hover { background: var(--primary); color: white; border-color: var(--primary); } | |
| .model-info { font-size: 12px; color: #6b7280; margin-top: 4px; } | |
| .model-info .tag { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-right: 6px; } | |
| .tag.nvidia { background: #76b90020; color: #76b900; } | |
| .tag.baidu { background: #2932e120; color: #2932e1; } | |
| .tag.anthropic { background: #d4a57420; color: #c47a43; } | |
| .info-bar { font-size: 12px; color: #9ca3af; text-align: center; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>🦀 Hermès Agent</h1> | |
| <span class="badge">HF Free · 多模型</span> | |
| </header> | |
| <div class="container"> | |
| <div class="settings"> | |
| <h3>⚙️ 模型与 API 设置</h3> | |
| <div class="provider-tabs"> | |
| <button class="provider-tab active" data-provider="anthropic" onclick="switchProvider('anthropic')">🤖 Anthropic</button> | |
| <button class="provider-tab" data-provider="nvidia" onclick="switchProvider('nvidia')">🔋 NVIDIA NIM</button> | |
| <button class="provider-tab" data-provider="baidu" onclick="switchProvider('baidu')">🌐 百度 CoBoom</button> | |
| </div> | |
| <div class="settings-row"> | |
| <div> | |
| <label for="model">模型</label> | |
| <select id="model" onchange="updateModelInfo()"></select> | |
| <div class="model-info" id="modelInfo"></div> | |
| </div> | |
| </div> | |
| <div class="api-key-section"> | |
| <label for="apiKey">API Key <span id="keyHint"></span></label> | |
| <input type="password" id="apiKey" placeholder="输入 API Key..." oninput="saveSettings()"> | |
| <div class="field-hint" id="keyHelp"></div> | |
| </div> | |
| </div> | |
| <div class="chat-container"> | |
| <div class="chat-header"> | |
| <span class="dot"></span> | |
| <span id="statusText">选择模型并输入 API Key 后开始对话</span> | |
| </div> | |
| <div class="chat-messages" id="messages"> | |
| <div class="msg system">🦀 Hermès Agent · 多模型 AI 助手<br><span style="font-size:12px;opacity:0.7">支持 Anthropic / NVIDIA NIM / 百度 CoBoom</span></div> | |
| </div> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <span id="loadingText">Hermès 思考中...</span> | |
| </div> | |
| <div class="chat-input-area"> | |
| <textarea id="input" rows="1" placeholder="输入消息,按 Enter 发送..." onkeydown="handleKeydown(event)"></textarea> | |
| <button class="send-btn" id="sendBtn" onclick="sendMessage()">发送</button> | |
| </div> | |
| </div> | |
| <div style="background:var(--surface);border-radius:12px;padding:16px 20px;box-shadow:0 1px 3px rgba(0,0,0,0.08);"> | |
| <div style="font-size:12px;color:#9ca3af;margin-bottom:10px;text-transform:uppercase;letter-spacing:0.5px">💡 示例问题</div> | |
| <div class="examples"> | |
| <span class="example-chip" onclick="askExample(this.textContent)">你好,你叫什么名字?</span> | |
| <span class="example-chip" onclick="askExample(this.textContent)">帮我写一个 Python Hello World</span> | |
| <span class="example-chip" onclick="askExample(this.textContent)">解释什么是大语言模型</span> | |
| <span class="example-chip" onclick="askExample(this.textContent)">用 Python 实现快速排序</span> | |
| <span class="example-chip" onclick="askExample(this.textContent)">解释什么是 REST API</span> | |
| </div> | |
| </div> | |
| <div class="info-bar"> | |
| API Key 仅存储在浏览器本地 · 直连各大 API 提供商 | |
| </div> | |
| </div> | |
| <script> | |
| // ============================================================================ | |
| // Model Configurations | |
| // ============================================================================ | |
| const PROVIDERS = { | |
| anthropic: { | |
| name: 'Anthropic', | |
| baseUrl: 'https://api.anthropic.com/v1/messages', | |
| keyPlaceholder: 'sk-ant-...', | |
| keyHelp: '访问 <a href="https://console.anthropic.com/" target="_blank" style="color:var(--primary)">console.anthropic.com</a> 创建免费 API Key', | |
| models: [ | |
| { id: 'claude-sonnet-4-7-2027', name: 'Claude Sonnet 4', size: '~120B', tag: 'anthropic', hint: '最新旗舰模型' }, | |
| { id: 'claude-3-5-sonnet-2027-06-20', name: 'Claude 3.5 Sonnet', size: '~70B', tag: 'anthropic', hint: '平衡性能与速度' }, | |
| { id: 'claude-3-5-haiku-2027-03-07', name: 'Claude 3.5 Haiku', size: '~20B', tag: 'anthropic', hint: '快速响应' }, | |
| ] | |
| }, | |
| nvidia: { | |
| name: 'NVIDIA NIM', | |
| baseUrl: 'https://integrate.api.nvidia.com/v1/chat/completions', | |
| keyPlaceholder: 'nvapi-...', | |
| keyHelp: '访问 <a href="https://build.nvidia.com/" target="_blank" style="color:var(--primary)">build.nvidia.com</a> 获取免费 API Key', | |
| models: [ | |
| { id: 'nvidia/llama-3.1-nemotron-70b-instruct', name: 'Nemotron 70B', size: '70B', tag: 'nvidia', hint: 'NVIDIA 优化 Llama' }, | |
| { id: 'meta/llama-3.1-405b-instruct', name: 'Llama 3.1 405B', size: '405B', tag: 'nvidia', hint: '超大参数模型' }, | |
| { id: 'meta/llama-3.1-70b-instruct', name: 'Llama 3.1 70B', size: '70B', tag: 'nvidia', hint: '高性能开源' }, | |
| { id: 'mistralai/mixtral-8x22b-instruct-v0.1', name: 'Mixtral 8x22B', size: '8x22B', tag: 'nvidia', hint: 'MOE 架构' }, | |
| { id: 'google/gemma-2-27b-it', name: 'Gemma 2 27B', size: '27B', tag: 'nvidia', hint: 'Google 开源' }, | |
| ] | |
| }, | |
| baidu: { | |
| name: '百度 CoBoom', | |
| baseUrl: '', // user needs to provide their own endpoint | |
| keyPlaceholder: '输入 CoBoom API Key...', | |
| keyHelp: '访问 <a href="https://cloud.baidu.com/" target="_blank" style="color:var(--primary)">cloud.baidu.com</a> 获取 CoBoom API Key', | |
| models: [ | |
| { id: 'ernie-4.5-turbo', name: 'ERNIE 4.5 Turbo', size: '?', tag: 'baidu', hint: '文心一言 4.5 Turbo' }, | |
| { id: 'ernie-speed-pro', name: 'ERNIE Speed-Pro', size: '?', tag: 'baidu', hint: '高速响应版本' }, | |
| { id: 'ernie-lite-pro', name: 'ERNIE Lite-Pro', size: '?', tag: 'baidu', hint: '轻量高性能' }, | |
| { id: 'coboodle-code', name: 'CoBuddy 代码模型', size: '?', tag: 'baidu', hint: '百度代码专用模型' }, | |
| ] | |
| } | |
| }; | |
| // ============================================================================ | |
| // State | |
| // ============================================================================ | |
| let currentProvider = 'anthropic'; | |
| let messages = []; | |
| const SYSTEM_PROMPT = `你是一个友好的 AI 助手,名叫 Hermès 🦀。 | |
| 核心原则: | |
| 1. 简洁直接 - 结论先行,不需要废话 | |
| 2. 行动导向 - 有想法就执行 | |
| 3. 中文优先 - 用中文回答,除非用户用英文 | |
| 你的专长: | |
| - 信息查询与分析 | |
| - 代码编写与调试 | |
| - 写作与翻译 | |
| - 逻辑推理 | |
| 遇到不确定的问题,诚实说明,不要编造。`; | |
| // ============================================================================ | |
| // UI Functions | |
| // ============================================================================ | |
| function switchProvider(provider) { | |
| currentProvider = provider; | |
| // Update tabs | |
| document.querySelectorAll('.provider-tab').forEach(tab => { | |
| tab.classList.toggle('active', tab.dataset.provider === provider); | |
| }); | |
| // Update model dropdown | |
| const select = document.getElementById('model'); | |
| const config = PROVIDERS[provider]; | |
| select.innerHTML = config.models.map(m => | |
| `<option value="${m.id}">${m.name} (${m.size})</option>` | |
| ).join(''); | |
| // Update key fields | |
| document.getElementById('keyHint').textContent = `(${config.name})`; | |
| document.getElementById('keyHelp').innerHTML = config.keyHelp; | |
| updateModelInfo(); | |
| saveSettings(); | |
| } | |
| function updateModelInfo() { | |
| const modelId = document.getElementById('model').value; | |
| const config = PROVIDERS[currentProvider]; | |
| const model = config.models.find(m => m.id === modelId); | |
| if (model) { | |
| document.getElementById('modelInfo').innerHTML = | |
| `<span class="tag ${model.tag}">${model.tag.toUpperCase()}</span>${model.hint}`; | |
| } | |
| } | |
| function saveSettings() { | |
| localStorage.setItem('hermes_provider', currentProvider); | |
| localStorage.setItem('hermes_apiKey_' + currentProvider, document.getElementById('apiKey').value); | |
| localStorage.setItem('hermes_model_' + currentProvider, document.getElementById('model').value); | |
| } | |
| function loadSettings() { | |
| const savedProvider = localStorage.getItem('hermes_provider') || 'anthropic'; | |
| currentProvider = savedProvider; | |
| // Update tabs | |
| document.querySelectorAll('.provider-tab').forEach(tab => { | |
| tab.classList.toggle('active', tab.dataset.provider === savedProvider); | |
| }); | |
| // Fill provider config | |
| const config = PROVIDERS[savedProvider]; | |
| document.getElementById('keyHint').textContent = `(${config.name})`; | |
| document.getElementById('keyHelp').innerHTML = config.keyHelp; | |
| document.getElementById('model').innerHTML = config.models.map(m => | |
| `<option value="${m.id}">${m.name} (${m.size})</option>` | |
| ).join(''); | |
| // Restore saved values | |
| const savedKey = localStorage.getItem('hermes_apiKey_' + savedProvider) || ''; | |
| const savedModel = localStorage.getItem('hermes_model_' + savedProvider) || config.models[0].id; | |
| document.getElementById('apiKey').value = savedKey; | |
| document.getElementById('model').value = savedModel; | |
| updateModelInfo(); | |
| } | |
| loadSettings(); | |
| // ============================================================================ | |
| // Chat Functions | |
| // ============================================================================ | |
| function askExample(text) { | |
| const apiKey = document.getElementById('apiKey').value.trim(); | |
| if (!apiKey) { | |
| addMsg('system', '⚠️ 请先选择模型并输入对应的 API Key'); | |
| return; | |
| } | |
| document.getElementById('input').value = text; | |
| sendMessage(); | |
| } | |
| function handleKeydown(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| } | |
| function addMsg(role, content, modelTag) { | |
| const container = document.getElementById('messages'); | |
| const div = document.createElement('div'); | |
| div.className = `msg ${role}`; | |
| if (modelTag && role === 'assistant') { | |
| div.innerHTML = `<div class="model-tag">${modelTag}</div>${escapeHtml(content)}`; | |
| } else { | |
| div.textContent = content; | |
| } | |
| container.appendChild(div); | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| function setLoading(on, text) { | |
| document.getElementById('loading').className = on ? 'loading show' : 'loading'; | |
| if (text) document.getElementById('loadingText').textContent = text; | |
| document.getElementById('sendBtn').disabled = on; | |
| document.getElementById('input').disabled = on; | |
| } | |
| function setStatus(text) { | |
| document.getElementById('statusText').textContent = text; | |
| } | |
| // ============================================================================ | |
| // API Calls | |
| // ============================================================================ | |
| async function callAnthropic(apiKey, model, msgs) { | |
| const resp = await fetch('https://api.anthropic.com/v1/messages', { | |
| method: 'POST', | |
| headers: { | |
| 'x-api-key': apiKey, | |
| 'anthropic-version': '2023-06-01', | |
| 'content-type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| model: model, | |
| max_tokens: 4096, | |
| system: SYSTEM_PROMPT, | |
| messages: msgs, | |
| }), | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({})); | |
| throw new Error(err.error?.message || `API 错误 ${resp.status}`); | |
| } | |
| const data = await resp.json(); | |
| return data.content[0].text; | |
| } | |
| async function callNvidia(apiKey, model, msgs) { | |
| // Convert messages to openai-compatible format | |
| const openaiMsgs = [{ role: 'system', content: SYSTEM_PROMPT }]; | |
| for (const m of msgs) { | |
| openaiMsgs.push({ role: m.role, content: m.content }); | |
| } | |
| const resp = await fetch('https://integrate.api.nvidia.com/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': 'Bearer ' + apiKey, | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| model: model, | |
| messages: openaiMsgs, | |
| max_tokens: 4096, | |
| temperature: 0.7, | |
| stream: false, | |
| }), | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({})); | |
| throw new Error(err.error?.message || `API 错误 ${resp.status}`); | |
| } | |
| const data = await resp.json(); | |
| return data.choices[0].message.content; | |
| } | |
| async function callBaidu(apiKey, model, msgs) { | |
| // Baidu ERNIE uses different API - show guidance for user to configure | |
| const baseUrls = { | |
| 'ernie-4.5-turbo': 'https://qianfan.baidubce.com/v2/app/conversation/v2/chat', | |
| 'ernie-speed-pro': 'https://qianfan.baidubce.com/v2/app/conversation/v2/chat', | |
| 'ernie-lite-pro': 'https://qianfan.baidubce.com/v2/app/conversation/v2/chat', | |
| }; | |
| // For CoBoom code model, use Wenxin API format | |
| const systemMsg = { role: 'system', content: SYSTEM_PROMPT }; | |
| const openaiMsgs = [systemMsg, ...msgs.map(m => ({ role: m.role, content: m.content }))]; | |
| // Try standard ERNIE API endpoint | |
| const endpoint = baseUrls[model] || 'https://qianfan.baidubce.com/v2/app/conversation/v2/chat'; | |
| const resp = await fetch(endpoint, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': 'Bearer ' + apiKey, | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| model: model, | |
| messages: openaiMsgs, | |
| max_tokens: 4096, | |
| }), | |
| }); | |
| if (!resp.ok) { | |
| const text = await resp.text(); | |
| throw new Error(`百度 API 错误 ${resp.status}: ${text}`); | |
| } | |
| const data = await resp.json(); | |
| return data.result || JSON.stringify(data); | |
| } | |
| async function sendMessage() { | |
| const input = document.getElementById('input'); | |
| const text = input.value.trim(); | |
| const apiKey = document.getElementById('apiKey').value.trim(); | |
| const model = document.getElementById('model').value; | |
| if (!text) return; | |
| if (!apiKey) { | |
| addMsg('system', '⚠️ 请先输入 API Key'); | |
| return; | |
| } | |
| addMsg('user', text); | |
| input.value = ''; | |
| setLoading(true, 'Hermès 思考中...'); | |
| setStatus(`正在调用 ${PROVIDERS[currentProvider].name}...`); | |
| const userMsg = { role: 'user', content: text }; | |
| let reply; | |
| let modelLabel = model; | |
| try { | |
| switch (currentProvider) { | |
| case 'anthropic': | |
| reply = await callAnthropic(apiKey, model, messages.concat(userMsg)); | |
| break; | |
| case 'nvidia': | |
| reply = await callNvidia(apiKey, model, messages.concat(userMsg)); | |
| break; | |
| case 'baidu': | |
| reply = await callBaidu(apiKey, model, messages.concat(userMsg)); | |
| break; | |
| } | |
| messages.push(userMsg, { role: 'assistant', content: reply }); | |
| const config = PROVIDERS[currentProvider]; | |
| const modelObj = config.models.find(m => m.id === model); | |
| modelLabel = modelObj ? modelObj.name : model; | |
| addMsg('assistant', reply, modelLabel); | |
| setStatus(`已就绪 · ${modelLabel}`); | |
| } catch (err) { | |
| addMsg('error', `❌ ${err.message}`); | |
| setStatus('请求失败'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |