aaxaxax commited on
Commit
7e57cf8
·
1 Parent(s): bff69bd

feat: add NVIDIA NIM and Baidu CoBoom multi-provider support

Browse files
Files changed (2) hide show
  1. README.md +41 -18
  2. index.html +331 -79
README.md CHANGED
@@ -10,30 +10,53 @@ pinned: false
10
 
11
  # 🦀 Hermès Agent
12
 
13
- 基于 **Claude** 的 AI 对话助手 · 纯前端实现 · 零服务器成本
14
 
15
- ## 特点
16
 
17
- - 🌐 **纯前端** - 无服务器,部署在 Hugging Face Pages(免费)
18
- - 🔒 **隐私安全** - API Key 仅存储在你的浏览器中
19
- - ⚡ **实时响应** - 直连 Anthropic API,无中间层
20
- - 📱 **响应式设计** - 适配桌面和移动端
21
- - 💾 **上下文记忆** - 支持多轮对话
 
22
 
23
- ## 使用方法
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- 1. 在上方设置中粘贴你的 **Anthropic API Key**
26
- 2. 选择模型
27
- 3. 开始对话!
 
 
 
 
28
 
29
- ## 获取 API Key
 
 
30
 
31
- 访问 [Anthropic Console](https://console.anthropic.com/) 创建免费 API Key
 
 
 
32
 
33
- ## 技术实现
34
 
35
- - **前端**: 原生 HTML/CSS/JavaScript(零依赖)
36
- - **API**: Anthropic Messages API(直连)
37
- - **部署**: Hugging Face Spaces (Static)
 
 
38
 
39
- > 💡 这是 Hermès Agent 的 Web 界面版。核心 Agent 代码托管于 [cntalk/hermes-agent](https://github.com/cntalk/hermes-agent)
 
10
 
11
  # 🦀 Hermès Agent
12
 
13
+ 基于 **Claude** 的 AI 对话助手 · 多模型支持 · 纯前端实现 · 零服务器成本
14
 
15
+ ## 支持的模型
16
 
17
+ ### 🤖 Anthropic (Claude)
18
+ | 模型 | 规模 | 特点 |
19
+ |------|------|------|
20
+ | Claude Sonnet 4 | ~120B | 最新旗舰模型 |
21
+ | Claude 3.5 Sonnet | ~70B | 平衡性能与速度 |
22
+ | Claude 3.5 Haiku | ~20B | 快速响应 |
23
 
24
+ **API Key:** [console.anthropic.com](https://console.anthropic.com/) · 有免费额度
25
+
26
+ ### 🔋 NVIDIA NIM (免费 GPU 推理)
27
+ | 模型 | 规模 | 特点 |
28
+ |------|------|------|
29
+ | Nemotron 70B | 70B | NVIDIA 优化 Llama |
30
+ | Llama 3.1 405B | 405B | 超大参数模型 |
31
+ | Llama 3.1 70B | 70B | 高性能开源 |
32
+ | Mixtral 8x22B | 8x22B | MOE 架构 |
33
+ | Gemma 2 27B | 27B | Google 开源 |
34
+
35
+ **API Key:** [build.nvidia.com](https://build.nvidia.com/) · 部分模型免费
36
 
37
+ ### 🌐 百度 CoBoom
38
+ | 模型 | 特点 |
39
+ |------|------|
40
+ | ERNIE 4.5 Turbo | 文心一言 4.5 |
41
+ | ERNIE Speed-Pro | 高速响应 |
42
+ | ERNIE Lite-Pro | 轻量高性能 |
43
+ | CoBuddy 代码模型 | 百度代码专用 |
44
 
45
+ **API Key:** [cloud.baidu.com](https://cloud.baidu.com/)
46
+
47
+ ## 使用方法
48
 
49
+ 1. 选择模型提供商(Anthropic / NVIDIA NIM / 百度)
50
+ 2. 选择具体模型
51
+ 3. 输入对应的 API Key
52
+ 4. 开始对话!
53
 
54
+ ## 技术特点
55
 
56
+ - 🌐 **前端** - 无服务器,部署在 Hugging Face Pages
57
+ - 🔒 **隐私安全** - API Key 仅存储在浏览器本地
58
+ - **多模型** - 一个界面切换多个提供商
59
+ - 💾 **上下文** - 支持多轮对话
60
+ - 📱 **响应式** - 适配桌面和移动端
61
 
62
+ > 💡 直连各大 API 提供商,无中间层,零服务器成
index.html CHANGED
@@ -20,33 +20,50 @@
20
  header h1 { font-size: 20px; font-weight: 700; }
21
  header .badge { background: rgba(255,255,255,0.2); padding: 4px 12px; border-radius: 20px; font-size: 12px; }
22
 
23
- .container { max-width: 860px; margin: 0 auto; padding: 24px; width: 100%; flex: 1; display: flex; flex-direction: column; gap: 16px; }
24
 
25
  .settings { background: var(--surface); border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
26
- .settings h3 { font-size: 14px; color: #6b7280; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
27
  .settings-grid { display: grid; grid-template-columns: 1fr 180px; gap: 12px; }
28
- @media (max-width: 600px) { .settings-grid { grid-template-columns: 1fr; } }
 
 
 
 
 
29
 
30
- input[type="text"], input[type="password"], select, textarea {
31
- width: 100%; padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; outline: none; transition: border-color 0.2s;
32
  }
33
  input:focus, select:focus, textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(139,92,246,0.1); }
34
  select { cursor: pointer; }
 
 
 
 
 
 
 
 
 
 
35
 
36
  .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; }
37
- .chat-header { padding: 14px 20px; border-bottom: 1px solid var(--border); font-size: 14px; color: #6b7280; display: flex; align-items: center; gap: 8px; }
38
  .chat-header .dot { width: 8px; height: 8px; border-radius: 50%; background: #10b981; }
39
- .chat-messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
40
- .msg { max-width: 80%; padding: 12px 16px; border-radius: 14px; line-height: 1.6; font-size: 14px; white-space: pre-wrap; word-break: break-word; }
41
  .msg.user { align-self: flex-end; background: var(--primary); color: white; border-bottom-right-radius: 4px; }
42
  .msg.assistant { align-self: flex-start; background: #f3f4f6; border-bottom-left-radius: 4px; }
43
  .msg.error { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; }
44
- .msg.system { align-self: center; background: transparent; color: #9ca3af; font-size: 13px; text-align: center; }
 
 
45
 
46
- .chat-input-area { padding: 16px 20px; border-top: 1px solid var(--border); display: flex; gap: 10px; }
47
  .chat-input-area textarea { resize: none; flex: 1; height: 48px; }
48
  .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; }
49
- .send-btn:hover { opacity: 0.9; }
50
  .send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
51
 
52
  .loading { display: none; align-items: center; gap: 8px; color: #6b7280; font-size: 13px; padding: 8px 0; }
@@ -55,9 +72,15 @@
55
  @keyframes spin { to { transform: rotate(360deg); } }
56
 
57
  .examples { display: flex; flex-wrap: wrap; gap: 8px; }
58
- .example-chip { background: #f3f4f6; border: 1px solid var(--border); padding: 6px 14px; border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.2s; }
59
  .example-chip:hover { background: var(--primary); color: white; border-color: var(--primary); }
60
 
 
 
 
 
 
 
61
  .info-bar { font-size: 12px; color: #9ca3af; text-align: center; }
62
  </style>
63
  </head>
@@ -65,46 +88,46 @@
65
 
66
  <header>
67
  <h1>🦀 Hermès Agent</h1>
68
- <span class="badge">Claude · HF Free Tier</span>
69
  </header>
70
 
71
  <div class="container">
 
72
  <div class="settings">
73
- <h3>⚙️ 设置</h3>
74
- <div class="settings-grid">
75
- <div>
76
- <input type="password" id="apiKey" placeholder="sk-ant-... (粘贴你的 Anthropic API Key)" oninput="saveSettings()">
77
- <div style="font-size:12px;color:#9ca3af;margin-top:6px;">Key 仅存储在本地浏览器中,不会发送给任何第三方</div>
78
- </div>
 
 
 
79
  <div>
80
- <select id="model" onchange="saveSettings()">
81
- <option value="claude-sonnet-4-7-2027">Claude Sonnet 4</option>
82
- <option value="claude-3-5-sonnet-2027-06-20">Claude 3.5 Sonnet</option>
83
- <option value="claude-3-5-haiku-2027-03-07">Claude 3.5 Haiku</option>
84
- </select>
85
  </div>
86
  </div>
87
- <div style="margin-top:12px;">
88
- <div style="font-size:12px;color:#6b7280;margin-bottom:8px;">💡 示例问题</div>
89
- <div class="examples">
90
- <span class="example-chip" onclick="askExample(this.textContent)">你好,你叫什么名字?</span>
91
- <span class="example-chip" onclick="askExample(this.textContent)">帮我写一个 Python Hello World</span>
92
- <span class="example-chip" onclick="askExample(this.textContent)">解释什么是大语言模型</span>
93
- </div>
94
  </div>
95
  </div>
96
 
97
  <div class="chat-container">
98
  <div class="chat-header">
99
  <span class="dot"></span>
100
- <span id="statusText">基于 Claude AI 对话助手</span>
101
  </div>
102
  <div class="chat-messages" id="messages">
103
- <div class="msg system">🦀 Hermès Agent 已就绪,请输入你的问题</div>
104
  </div>
105
  <div class="loading" id="loading">
106
  <div class="spinner"></div>
107
- <span>Hermès 思考中...</span>
108
  </div>
109
  <div class="chat-input-area">
110
  <textarea id="input" rows="1" placeholder="输入消息,按 Enter 发送..." onkeydown="handleKeydown(event)"></textarea>
@@ -112,33 +135,165 @@
112
  </div>
113
  </div>
114
 
 
 
 
 
 
 
 
 
 
 
 
115
  <div class="info-bar">
116
- <strong>Hermès Agent</strong> 驱动 · 运行在 Hugging Face Pages · 数据留存在你的浏览器
117
  </div>
118
  </div>
119
 
120
  <script>
121
- const SYSTEM_PROMPT = `你是一个友好的 AI 助手,名叫 Hermès 🦀。核心原则:1. 简洁直接 - 结论先行 2. 行动导向 3. 中文优先。专长:信息查询、代码编写、写作翻译、逻辑推理。不确定时诚实说明,不编造。`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
 
 
 
 
 
123
  let messages = [];
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  function saveSettings() {
126
- localStorage.setItem('hermes_apiKey', document.getElementById('apiKey').value);
127
- localStorage.setItem('hermes_model', document.getElementById('model').value);
 
128
  }
129
 
130
  function loadSettings() {
131
- const savedKey = localStorage.getItem('hermes_apiKey') || '';
132
- const savedModel = localStorage.getItem('hermes_model') || 'claude-sonnet-4-7-2027';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  document.getElementById('apiKey').value = savedKey;
134
  document.getElementById('model').value = savedModel;
 
135
  }
136
  loadSettings();
137
 
 
 
 
 
138
  function askExample(text) {
139
- const apiKey = document.getElementById('apiKey').value;
140
  if (!apiKey) {
141
- addMessage('system', '⚠️ 请先在设置中输入 Anthropic API Key');
142
  return;
143
  }
144
  document.getElementById('input').value = text;
@@ -152,18 +307,28 @@ function handleKeydown(e) {
152
  }
153
  }
154
 
155
- function addMessage(role, content) {
156
  const container = document.getElementById('messages');
157
  const div = document.createElement('div');
158
  div.className = `msg ${role}`;
159
- div.textContent = content;
 
 
 
 
160
  container.appendChild(div);
161
  container.scrollTop = container.scrollHeight;
162
- return div;
163
  }
164
 
165
- function setLoading(on) {
 
 
 
 
 
 
166
  document.getElementById('loading').className = on ? 'loading show' : 'loading';
 
167
  document.getElementById('sendBtn').disabled = on;
168
  document.getElementById('input').disabled = on;
169
  }
@@ -172,6 +337,99 @@ function setStatus(text) {
172
  document.getElementById('statusText').textContent = text;
173
  }
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  async function sendMessage() {
176
  const input = document.getElementById('input');
177
  const text = input.value.trim();
@@ -180,47 +438,41 @@ async function sendMessage() {
180
 
181
  if (!text) return;
182
  if (!apiKey) {
183
- addMessage('system', '⚠️ 请先在设置中输入 Anthropic API Key');
184
  return;
185
  }
186
 
187
- addMessage('user', text);
188
  input.value = '';
189
- setLoading(true);
190
- setStatus('正在连接 Claude...');
191
-
192
- const payload = {
193
- model: model,
194
- max_tokens: 4096,
195
- system: SYSTEM_PROMPT,
196
- messages: [...messages, { role: 'user', content: text }]
197
- };
198
 
199
  try {
200
- const resp = await fetch('https://api.anthropic.com/v1/messages', {
201
- method: 'POST',
202
- headers: {
203
- 'x-api-key': apiKey,
204
- 'anthropic-version': '2023-06-01',
205
- 'content-type': 'application/json',
206
- },
207
- body: JSON.stringify(payload),
208
- });
209
-
210
- if (!resp.ok) {
211
- const err = await resp.json().catch(() => ({}));
212
- throw new Error(err.error?.message || `API 错误 ${resp.status}`);
213
  }
214
-
215
- const data = await resp.json();
216
- const reply = data.content[0].text;
217
- messages.push({ role: 'user', content: text });
218
- messages.push({ role: 'assistant', content: reply });
219
- addMessage('assistant', reply);
220
- setStatus('基于 Claude 的 AI 对话助手');
221
  } catch (err) {
222
- addMessage('error', `❌ ${err.message}`);
223
- setStatus('连接出错');
224
  } finally {
225
  setLoading(false);
226
  }
 
20
  header h1 { font-size: 20px; font-weight: 700; }
21
  header .badge { background: rgba(255,255,255,0.2); padding: 4px 12px; border-radius: 20px; font-size: 12px; }
22
 
23
+ .container { max-width: 900px; margin: 0 auto; padding: 24px; width: 100%; flex: 1; display: flex; flex-direction: column; gap: 16px; }
24
 
25
  .settings { background: var(--surface); border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
26
+ .settings h3 { font-size: 13px; color: #9ca3af; margin-bottom: 14px; text-transform: uppercase; letter-spacing: 0.5px; }
27
  .settings-grid { display: grid; grid-template-columns: 1fr 180px; gap: 12px; }
28
+ .settings-row { display: flex; gap: 12px; flex-wrap: wrap; }
29
+ .settings-row > * { flex: 1; min-width: 200px; }
30
+ @media (max-width: 640px) {
31
+ .settings-grid { grid-template-columns: 1fr; }
32
+ .settings-row > * { min-width: 100%; }
33
+ }
34
 
35
+ input, select, textarea {
36
+ 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);
37
  }
38
  input:focus, select:focus, textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(139,92,246,0.1); }
39
  select { cursor: pointer; }
40
+ .field-hint { font-size: 11px; color: #9ca3af; margin-top: 5px; }
41
+ .field-hint code { background: #f3f4f6; padding: 1px 5px; border-radius: 3px; font-size: 11px; }
42
+
43
+ .provider-tabs { display: flex; gap: 4px; margin-bottom: 14px; background: #f3f4f6; padding: 4px; border-radius: 10px; }
44
+ .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; }
45
+ .provider-tab.active { background: var(--surface); color: var(--primary); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
46
+ .provider-tab:hover:not(.active) { color: var(--text); }
47
+
48
+ .api-key-section { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 12px; }
49
+ .api-key-section label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; }
50
 
51
  .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; }
52
+ .chat-header { padding: 14px 20px; border-bottom: 1px solid var(--border); font-size: 13px; color: #6b7280; display: flex; align-items: center; gap: 8px; }
53
  .chat-header .dot { width: 8px; height: 8px; border-radius: 50%; background: #10b981; }
54
+ .chat-messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 14px; }
55
+ .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; }
56
  .msg.user { align-self: flex-end; background: var(--primary); color: white; border-bottom-right-radius: 4px; }
57
  .msg.assistant { align-self: flex-start; background: #f3f4f6; border-bottom-left-radius: 4px; }
58
  .msg.error { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; }
59
+ .msg.system { align-self: center; background: transparent; color: #9ca3af; font-size: 13px; text-align: center; max-width: 100%; }
60
+ .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; }
61
+ .msg.user .model-tag { background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.8); }
62
 
63
+ .chat-input-area { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; gap: 10px; }
64
  .chat-input-area textarea { resize: none; flex: 1; height: 48px; }
65
  .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; }
66
+ .send-btn:hover { opacity: 0.88; }
67
  .send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
68
 
69
  .loading { display: none; align-items: center; gap: 8px; color: #6b7280; font-size: 13px; padding: 8px 0; }
 
72
  @keyframes spin { to { transform: rotate(360deg); } }
73
 
74
  .examples { display: flex; flex-wrap: wrap; gap: 8px; }
75
+ .example-chip { background: #f3f4f6; border: 1px solid var(--border); padding: 6px 14px; border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.15s; }
76
  .example-chip:hover { background: var(--primary); color: white; border-color: var(--primary); }
77
 
78
+ .model-info { font-size: 12px; color: #6b7280; margin-top: 4px; }
79
+ .model-info .tag { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-right: 6px; }
80
+ .tag.nvidia { background: #76b90020; color: #76b900; }
81
+ .tag.baidu { background: #2932e120; color: #2932e1; }
82
+ .tag.anthropic { background: #d4a57420; color: #c47a43; }
83
+
84
  .info-bar { font-size: 12px; color: #9ca3af; text-align: center; }
85
  </style>
86
  </head>
 
88
 
89
  <header>
90
  <h1>🦀 Hermès Agent</h1>
91
+ <span class="badge">HF Free · 多模型</span>
92
  </header>
93
 
94
  <div class="container">
95
+
96
  <div class="settings">
97
+ <h3>⚙️ 模型与 API 设置</h3>
98
+
99
+ <div class="provider-tabs">
100
+ <button class="provider-tab active" data-provider="anthropic" onclick="switchProvider('anthropic')">🤖 Anthropic</button>
101
+ <button class="provider-tab" data-provider="nvidia" onclick="switchProvider('nvidia')">🔋 NVIDIA NIM</button>
102
+ <button class="provider-tab" data-provider="baidu" onclick="switchProvider('baidu')">🌐 百度 CoBoom</button>
103
+ </div>
104
+
105
+ <div class="settings-row">
106
  <div>
107
+ <label for="model">模型</label>
108
+ <select id="model" onchange="updateModelInfo()"></select>
109
+ <div class="model-info" id="modelInfo"></div>
 
 
110
  </div>
111
  </div>
112
+
113
+ <div class="api-key-section">
114
+ <label for="apiKey">API Key <span id="keyHint"></span></label>
115
+ <input type="password" id="apiKey" placeholder="输入 API Key..." oninput="saveSettings()">
116
+ <div class="field-hint" id="keyHelp"></div>
 
 
117
  </div>
118
  </div>
119
 
120
  <div class="chat-container">
121
  <div class="chat-header">
122
  <span class="dot"></span>
123
+ <span id="statusText">选择模型并输入 API Key 后开始对话</span>
124
  </div>
125
  <div class="chat-messages" id="messages">
126
+ <div class="msg system">🦀 Hermès Agent · 多模型 AI 助手<br><span style="font-size:12px;opacity:0.7">支持 Anthropic / NVIDIA NIM / 百度 CoBoom</span></div>
127
  </div>
128
  <div class="loading" id="loading">
129
  <div class="spinner"></div>
130
+ <span id="loadingText">Hermès 思考中...</span>
131
  </div>
132
  <div class="chat-input-area">
133
  <textarea id="input" rows="1" placeholder="输入消息,按 Enter 发送..." onkeydown="handleKeydown(event)"></textarea>
 
135
  </div>
136
  </div>
137
 
138
+ <div style="background:var(--surface);border-radius:12px;padding:16px 20px;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
139
+ <div style="font-size:12px;color:#9ca3af;margin-bottom:10px;text-transform:uppercase;letter-spacing:0.5px">💡 示例问题</div>
140
+ <div class="examples">
141
+ <span class="example-chip" onclick="askExample(this.textContent)">你好,你叫什么名字?</span>
142
+ <span class="example-chip" onclick="askExample(this.textContent)">帮我写一个 Python Hello World</span>
143
+ <span class="example-chip" onclick="askExample(this.textContent)">解释什么是大语言模型</span>
144
+ <span class="example-chip" onclick="askExample(this.textContent)">用 Python 实现快速排序</span>
145
+ <span class="example-chip" onclick="askExample(this.textContent)">解释什么是 REST API</span>
146
+ </div>
147
+ </div>
148
+
149
  <div class="info-bar">
150
+ API Key 在浏览器本地 · 直连各大 API 提供商
151
  </div>
152
  </div>
153
 
154
  <script>
155
+ // ============================================================================
156
+ // Model Configurations
157
+ // ============================================================================
158
+
159
+ const PROVIDERS = {
160
+ anthropic: {
161
+ name: 'Anthropic',
162
+ baseUrl: 'https://api.anthropic.com/v1/messages',
163
+ keyPlaceholder: 'sk-ant-...',
164
+ keyHelp: '访问 <a href="https://console.anthropic.com/" target="_blank" style="color:var(--primary)">console.anthropic.com</a> 创建免费 API Key',
165
+ models: [
166
+ { id: 'claude-sonnet-4-7-2027', name: 'Claude Sonnet 4', size: '~120B', tag: 'anthropic', hint: '最新旗舰模型' },
167
+ { id: 'claude-3-5-sonnet-2027-06-20', name: 'Claude 3.5 Sonnet', size: '~70B', tag: 'anthropic', hint: '平衡性能与速度' },
168
+ { id: 'claude-3-5-haiku-2027-03-07', name: 'Claude 3.5 Haiku', size: '~20B', tag: 'anthropic', hint: '快速响应' },
169
+ ]
170
+ },
171
+ nvidia: {
172
+ name: 'NVIDIA NIM',
173
+ baseUrl: 'https://integrate.api.nvidia.com/v1/chat/completions',
174
+ keyPlaceholder: 'nvapi-...',
175
+ keyHelp: '访问 <a href="https://build.nvidia.com/" target="_blank" style="color:var(--primary)">build.nvidia.com</a> 获取免费 API Key',
176
+ models: [
177
+ { id: 'nvidia/llama-3.1-nemotron-70b-instruct', name: 'Nemotron 70B', size: '70B', tag: 'nvidia', hint: 'NVIDIA 优化 Llama' },
178
+ { id: 'meta/llama-3.1-405b-instruct', name: 'Llama 3.1 405B', size: '405B', tag: 'nvidia', hint: '超大参数模型' },
179
+ { id: 'meta/llama-3.1-70b-instruct', name: 'Llama 3.1 70B', size: '70B', tag: 'nvidia', hint: '高性能开源' },
180
+ { id: 'mistralai/mixtral-8x22b-instruct-v0.1', name: 'Mixtral 8x22B', size: '8x22B', tag: 'nvidia', hint: 'MOE 架构' },
181
+ { id: 'google/gemma-2-27b-it', name: 'Gemma 2 27B', size: '27B', tag: 'nvidia', hint: 'Google 开源' },
182
+ ]
183
+ },
184
+ baidu: {
185
+ name: '百度 CoBoom',
186
+ baseUrl: '', // user needs to provide their own endpoint
187
+ keyPlaceholder: '输入 CoBoom API Key...',
188
+ keyHelp: '访问 <a href="https://cloud.baidu.com/" target="_blank" style="color:var(--primary)">cloud.baidu.com</a> 获取 CoBoom API Key',
189
+ models: [
190
+ { id: 'ernie-4.5-turbo', name: 'ERNIE 4.5 Turbo', size: '?', tag: 'baidu', hint: '文心一言 4.5 Turbo' },
191
+ { id: 'ernie-speed-pro', name: 'ERNIE Speed-Pro', size: '?', tag: 'baidu', hint: '高速响应版本' },
192
+ { id: 'ernie-lite-pro', name: 'ERNIE Lite-Pro', size: '?', tag: 'baidu', hint: '轻量高性能' },
193
+ { id: 'coboodle-code', name: 'CoBuddy 代码模型', size: '?', tag: 'baidu', hint: '百度代码专用模型' },
194
+ ]
195
+ }
196
+ };
197
 
198
+ // ============================================================================
199
+ // State
200
+ // ============================================================================
201
+
202
+ let currentProvider = 'anthropic';
203
  let messages = [];
204
 
205
+ const SYSTEM_PROMPT = `你是一个友好的 AI 助手,名叫 Hermès 🦀。
206
+
207
+ 核心原则:
208
+ 1. 简洁直接 - 结论先行,不需要废话
209
+ 2. 行动导向 - 有想法就执行
210
+ 3. 中文优先 - 用中文回答,除非用户用英文
211
+
212
+ 你的专长:
213
+ - 信息查询与分析
214
+ - 代码编写与调试
215
+ - 写作与翻译
216
+ - 逻辑推理
217
+
218
+ 遇到不确定的问题,诚实说明,不要编造。`;
219
+
220
+ // ============================================================================
221
+ // UI Functions
222
+ // ============================================================================
223
+
224
+ function switchProvider(provider) {
225
+ currentProvider = provider;
226
+
227
+ // Update tabs
228
+ document.querySelectorAll('.provider-tab').forEach(tab => {
229
+ tab.classList.toggle('active', tab.dataset.provider === provider);
230
+ });
231
+
232
+ // Update model dropdown
233
+ const select = document.getElementById('model');
234
+ const config = PROVIDERS[provider];
235
+ select.innerHTML = config.models.map(m =>
236
+ `<option value="${m.id}">${m.name} (${m.size})</option>`
237
+ ).join('');
238
+
239
+ // Update key fields
240
+ document.getElementById('keyHint').textContent = `(${config.name})`;
241
+ document.getElementById('keyHelp').innerHTML = config.keyHelp;
242
+
243
+ updateModelInfo();
244
+ saveSettings();
245
+ }
246
+
247
+ function updateModelInfo() {
248
+ const modelId = document.getElementById('model').value;
249
+ const config = PROVIDERS[currentProvider];
250
+ const model = config.models.find(m => m.id === modelId);
251
+ if (model) {
252
+ document.getElementById('modelInfo').innerHTML =
253
+ `<span class="tag ${model.tag}">${model.tag.toUpperCase()}</span>${model.hint}`;
254
+ }
255
+ }
256
+
257
  function saveSettings() {
258
+ localStorage.setItem('hermes_provider', currentProvider);
259
+ localStorage.setItem('hermes_apiKey_' + currentProvider, document.getElementById('apiKey').value);
260
+ localStorage.setItem('hermes_model_' + currentProvider, document.getElementById('model').value);
261
  }
262
 
263
  function loadSettings() {
264
+ const savedProvider = localStorage.getItem('hermes_provider') || 'anthropic';
265
+ currentProvider = savedProvider;
266
+
267
+ // Update tabs
268
+ document.querySelectorAll('.provider-tab').forEach(tab => {
269
+ tab.classList.toggle('active', tab.dataset.provider === savedProvider);
270
+ });
271
+
272
+ // Fill provider config
273
+ const config = PROVIDERS[savedProvider];
274
+ document.getElementById('keyHint').textContent = `(${config.name})`;
275
+ document.getElementById('keyHelp').innerHTML = config.keyHelp;
276
+ document.getElementById('model').innerHTML = config.models.map(m =>
277
+ `<option value="${m.id}">${m.name} (${m.size})</option>`
278
+ ).join('');
279
+
280
+ // Restore saved values
281
+ const savedKey = localStorage.getItem('hermes_apiKey_' + savedProvider) || '';
282
+ const savedModel = localStorage.getItem('hermes_model_' + savedProvider) || config.models[0].id;
283
  document.getElementById('apiKey').value = savedKey;
284
  document.getElementById('model').value = savedModel;
285
+ updateModelInfo();
286
  }
287
  loadSettings();
288
 
289
+ // ============================================================================
290
+ // Chat Functions
291
+ // ============================================================================
292
+
293
  function askExample(text) {
294
+ const apiKey = document.getElementById('apiKey').value.trim();
295
  if (!apiKey) {
296
+ addMsg('system', '⚠️ 请先选择模型并输入对应的 API Key');
297
  return;
298
  }
299
  document.getElementById('input').value = text;
 
307
  }
308
  }
309
 
310
+ function addMsg(role, content, modelTag) {
311
  const container = document.getElementById('messages');
312
  const div = document.createElement('div');
313
  div.className = `msg ${role}`;
314
+ if (modelTag && role === 'assistant') {
315
+ div.innerHTML = `<div class="model-tag">${modelTag}</div>${escapeHtml(content)}`;
316
+ } else {
317
+ div.textContent = content;
318
+ }
319
  container.appendChild(div);
320
  container.scrollTop = container.scrollHeight;
 
321
  }
322
 
323
+ function escapeHtml(text) {
324
+ const div = document.createElement('div');
325
+ div.textContent = text;
326
+ return div.innerHTML;
327
+ }
328
+
329
+ function setLoading(on, text) {
330
  document.getElementById('loading').className = on ? 'loading show' : 'loading';
331
+ if (text) document.getElementById('loadingText').textContent = text;
332
  document.getElementById('sendBtn').disabled = on;
333
  document.getElementById('input').disabled = on;
334
  }
 
337
  document.getElementById('statusText').textContent = text;
338
  }
339
 
340
+ // ============================================================================
341
+ // API Calls
342
+ // ============================================================================
343
+
344
+ async function callAnthropic(apiKey, model, msgs) {
345
+ const resp = await fetch('https://api.anthropic.com/v1/messages', {
346
+ method: 'POST',
347
+ headers: {
348
+ 'x-api-key': apiKey,
349
+ 'anthropic-version': '2023-06-01',
350
+ 'content-type': 'application/json',
351
+ },
352
+ body: JSON.stringify({
353
+ model: model,
354
+ max_tokens: 4096,
355
+ system: SYSTEM_PROMPT,
356
+ messages: msgs,
357
+ }),
358
+ });
359
+ if (!resp.ok) {
360
+ const err = await resp.json().catch(() => ({}));
361
+ throw new Error(err.error?.message || `API 错误 ${resp.status}`);
362
+ }
363
+ const data = await resp.json();
364
+ return data.content[0].text;
365
+ }
366
+
367
+ async function callNvidia(apiKey, model, msgs) {
368
+ // Convert messages to openai-compatible format
369
+ const openaiMsgs = [{ role: 'system', content: SYSTEM_PROMPT }];
370
+ for (const m of msgs) {
371
+ openaiMsgs.push({ role: m.role, content: m.content });
372
+ }
373
+
374
+ const resp = await fetch('https://integrate.api.nvidia.com/v1/chat/completions', {
375
+ method: 'POST',
376
+ headers: {
377
+ 'Authorization': 'Bearer ' + apiKey,
378
+ 'Content-Type': 'application/json',
379
+ },
380
+ body: JSON.stringify({
381
+ model: model,
382
+ messages: openaiMsgs,
383
+ max_tokens: 4096,
384
+ temperature: 0.7,
385
+ stream: false,
386
+ }),
387
+ });
388
+ if (!resp.ok) {
389
+ const err = await resp.json().catch(() => ({}));
390
+ throw new Error(err.error?.message || `API 错误 ${resp.status}`);
391
+ }
392
+ const data = await resp.json();
393
+ return data.choices[0].message.content;
394
+ }
395
+
396
+ async function callBaidu(apiKey, model, msgs) {
397
+ // Baidu ERNIE uses different API - show guidance for user to configure
398
+ const baseUrls = {
399
+ 'ernie-4.5-turbo': 'https://qianfan.baidubce.com/v2/app/conversation/v2/chat',
400
+ 'ernie-speed-pro': 'https://qianfan.baidubce.com/v2/app/conversation/v2/chat',
401
+ 'ernie-lite-pro': 'https://qianfan.baidubce.com/v2/app/conversation/v2/chat',
402
+ };
403
+
404
+ // For CoBoom code model, use Wenxin API format
405
+ const systemMsg = { role: 'system', content: SYSTEM_PROMPT };
406
+ const openaiMsgs = [systemMsg, ...msgs.map(m => ({ role: m.role, content: m.content }))];
407
+
408
+ // Try standard ERNIE API endpoint
409
+ const endpoint = baseUrls[model] || 'https://qianfan.baidubce.com/v2/app/conversation/v2/chat';
410
+
411
+ const resp = await fetch(endpoint, {
412
+ method: 'POST',
413
+ headers: {
414
+ 'Authorization': 'Bearer ' + apiKey,
415
+ 'Content-Type': 'application/json',
416
+ },
417
+ body: JSON.stringify({
418
+ model: model,
419
+ messages: openaiMsgs,
420
+ max_tokens: 4096,
421
+ }),
422
+ });
423
+
424
+ if (!resp.ok) {
425
+ const text = await resp.text();
426
+ throw new Error(`百度 API 错误 ${resp.status}: ${text}`);
427
+ }
428
+
429
+ const data = await resp.json();
430
+ return data.result || JSON.stringify(data);
431
+ }
432
+
433
  async function sendMessage() {
434
  const input = document.getElementById('input');
435
  const text = input.value.trim();
 
438
 
439
  if (!text) return;
440
  if (!apiKey) {
441
+ addMsg('system', '⚠️ 请先输入 API Key');
442
  return;
443
  }
444
 
445
+ addMsg('user', text);
446
  input.value = '';
447
+ setLoading(true, 'Hermès 思考中...');
448
+ setStatus(`正在调用 ${PROVIDERS[currentProvider].name}...`);
449
+
450
+ const userMsg = { role: 'user', content: text };
451
+ let reply;
452
+ let modelLabel = model;
 
 
 
453
 
454
  try {
455
+ switch (currentProvider) {
456
+ case 'anthropic':
457
+ reply = await callAnthropic(apiKey, model, messages.concat(userMsg));
458
+ break;
459
+ case 'nvidia':
460
+ reply = await callNvidia(apiKey, model, messages.concat(userMsg));
461
+ break;
462
+ case 'baidu':
463
+ reply = await callBaidu(apiKey, model, messages.concat(userMsg));
464
+ break;
 
 
 
465
  }
466
+
467
+ messages.push(userMsg, { role: 'assistant', content: reply });
468
+ const config = PROVIDERS[currentProvider];
469
+ const modelObj = config.models.find(m => m.id === model);
470
+ modelLabel = modelObj ? modelObj.name : model;
471
+ addMsg('assistant', reply, modelLabel);
472
+ setStatus(`已就绪 · ${modelLabel}`);
473
  } catch (err) {
474
+ addMsg('error', `❌ ${err.message}`);
475
+ setStatus('请求失败');
476
  } finally {
477
  setLoading(false);
478
  }