andito HF Staff commited on
Commit
5fbe173
·
1 Parent(s): 5b2ced4

Let users change their API key and check that the key is valid

Browse files
src/reachy_mini_conversation_app/console.py CHANGED
@@ -217,6 +217,35 @@ class LocalStream:
217
  self._persist_api_key(key)
218
  return JSONResponse({"ok": True})
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  self._settings_initialized = True
221
 
222
  def launch(self) -> None:
 
217
  self._persist_api_key(key)
218
  return JSONResponse({"ok": True})
219
 
220
+ # POST /validate_api_key -> validate key without persisting it
221
+ @self._settings_app.post("/validate_api_key")
222
+ async def _validate_key(payload: ApiKeyPayload) -> JSONResponse:
223
+ key = (payload.openai_api_key or "").strip()
224
+ if not key:
225
+ return JSONResponse({"valid": False, "error": "empty_key"}, status_code=400)
226
+
227
+ # Try to validate by checking if we can fetch the models
228
+ try:
229
+ import httpx
230
+ headers = {
231
+ "Authorization": f"Bearer {key}",
232
+ "Content-Type": "application/json"
233
+ }
234
+ async with httpx.AsyncClient(timeout=10.0) as client:
235
+ response = await client.get(
236
+ "https://api.openai.com/v1/models",
237
+ headers=headers
238
+ )
239
+ if response.status_code == 200:
240
+ return JSONResponse({"valid": True})
241
+ elif response.status_code == 401:
242
+ return JSONResponse({"valid": False, "error": "invalid_api_key"}, status_code=401)
243
+ else:
244
+ return JSONResponse({"valid": False, "error": "validation_failed"}, status_code=response.status_code)
245
+ except Exception as e:
246
+ logger.warning(f"API key validation failed: {e}")
247
+ return JSONResponse({"valid": False, "error": "validation_error"}, status_code=500)
248
+
249
  self._settings_initialized = True
250
 
251
  def launch(self) -> None:
src/reachy_mini_conversation_app/static/index.html CHANGED
@@ -28,6 +28,7 @@
28
  <span class="chip chip-ok">Connected</span>
29
  </div>
30
  <p class="muted">OpenAI API key is already configured. You can jump straight to personalities.</p>
 
31
  </div>
32
 
33
  <div id="form-panel" class="panel hidden">
 
28
  <span class="chip chip-ok">Connected</span>
29
  </div>
30
  <p class="muted">OpenAI API key is already configured. You can jump straight to personalities.</p>
31
+ <button id="change-key-btn" class="ghost">Change API key</button>
32
  </div>
33
 
34
  <div id="form-panel" class="panel hidden">
src/reachy_mini_conversation_app/static/main.js CHANGED
@@ -57,6 +57,20 @@ async function waitForPersonalityData(timeoutMs = 15000) {
57
  }
58
  }
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  async function saveKey(key) {
61
  const body = { openai_api_key: key };
62
  const resp = await fetch("/openai_api_key", {
@@ -170,6 +184,7 @@ async function init() {
170
  const configuredPanel = document.getElementById("configured");
171
  const personalityPanel = document.getElementById("personality-panel");
172
  const saveBtn = document.getElementById("save-btn");
 
173
  const input = document.getElementById("api-key");
174
 
175
  // Personality elements
@@ -200,22 +215,55 @@ async function init() {
200
  show(configuredPanel, true);
201
  }
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  saveBtn.addEventListener("click", async () => {
204
  const key = input.value.trim();
205
  if (!key) {
206
  statusEl.textContent = "Please enter a valid key.";
207
  statusEl.className = "status warn";
 
208
  return;
209
  }
210
- statusEl.textContent = "Saving...";
211
  statusEl.className = "status";
 
212
  try {
 
 
 
 
 
 
 
 
 
 
 
 
213
  await saveKey(key);
214
  statusEl.textContent = "Saved. Reloading…";
215
  statusEl.className = "status ok";
216
  window.location.reload();
217
  } catch (e) {
218
- statusEl.textContent = "Failed to save key.";
 
 
 
 
 
219
  statusEl.className = "status error";
220
  }
221
  });
 
57
  }
58
  }
59
 
60
+ async function validateKey(key) {
61
+ const body = { openai_api_key: key };
62
+ const resp = await fetch("/validate_api_key", {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify(body),
66
+ });
67
+ const data = await resp.json().catch(() => ({}));
68
+ if (!resp.ok) {
69
+ throw new Error(data.error || "validation_failed");
70
+ }
71
+ return data;
72
+ }
73
+
74
  async function saveKey(key) {
75
  const body = { openai_api_key: key };
76
  const resp = await fetch("/openai_api_key", {
 
184
  const configuredPanel = document.getElementById("configured");
185
  const personalityPanel = document.getElementById("personality-panel");
186
  const saveBtn = document.getElementById("save-btn");
187
+ const changeKeyBtn = document.getElementById("change-key-btn");
188
  const input = document.getElementById("api-key");
189
 
190
  // Personality elements
 
215
  show(configuredPanel, true);
216
  }
217
 
218
+ // Handler for "Change API key" button
219
+ changeKeyBtn.addEventListener("click", () => {
220
+ show(configuredPanel, false);
221
+ show(formPanel, true);
222
+ input.value = "";
223
+ statusEl.textContent = "";
224
+ statusEl.className = "status";
225
+ });
226
+
227
+ // Remove error styling when user starts typing
228
+ input.addEventListener("input", () => {
229
+ input.classList.remove("error");
230
+ });
231
+
232
  saveBtn.addEventListener("click", async () => {
233
  const key = input.value.trim();
234
  if (!key) {
235
  statusEl.textContent = "Please enter a valid key.";
236
  statusEl.className = "status warn";
237
+ input.classList.add("error");
238
  return;
239
  }
240
+ statusEl.textContent = "Validating API key...";
241
  statusEl.className = "status";
242
+ input.classList.remove("error");
243
  try {
244
+ // First validate the key
245
+ const validation = await validateKey(key);
246
+ if (!validation.valid) {
247
+ statusEl.textContent = "Invalid API key. Please check your key and try again.";
248
+ statusEl.className = "status error";
249
+ input.classList.add("error");
250
+ return;
251
+ }
252
+
253
+ // If valid, save it
254
+ statusEl.textContent = "Key valid! Saving...";
255
+ statusEl.className = "status ok";
256
  await saveKey(key);
257
  statusEl.textContent = "Saved. Reloading…";
258
  statusEl.className = "status ok";
259
  window.location.reload();
260
  } catch (e) {
261
+ input.classList.add("error");
262
+ if (e.message === "invalid_api_key") {
263
+ statusEl.textContent = "Invalid API key. Please check your key and try again.";
264
+ } else {
265
+ statusEl.textContent = "Failed to validate/save key. Please try again.";
266
+ }
267
  statusEl.className = "status error";
268
  }
269
  });
src/reachy_mini_conversation_app/static/style.css CHANGED
@@ -163,6 +163,10 @@ textarea:focus {
163
  outline: none;
164
  box-shadow: 0 0 0 3px rgba(94, 240, 193, 0.15);
165
  }
 
 
 
 
166
  select option {
167
  background: #0b152a;
168
  color: var(--text);
 
163
  outline: none;
164
  box-shadow: 0 0 0 3px rgba(94, 240, 193, 0.15);
165
  }
166
+ input.error {
167
+ border-color: var(--error);
168
+ box-shadow: 0 0 0 3px rgba(255, 92, 112, 0.15);
169
+ }
170
  select option {
171
  background: #0b152a;
172
  color: var(--text);