apirrone commited on
Commit
50939e7
·
2 Parent(s): 70c6b85 5fbe173

Merge branch '62-appify-the-demo' into 62-spinoff-startup-personalities

Browse files
src/reachy_mini_conversation_app/console.py CHANGED
@@ -275,6 +275,35 @@ class LocalStream:
275
  self._persist_api_key(key)
276
  return JSONResponse({"ok": True})
277
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  self._settings_initialized = True
279
 
280
  def launch(self) -> None:
 
275
  self._persist_api_key(key)
276
  return JSONResponse({"ok": True})
277
 
278
+ # POST /validate_api_key -> validate key without persisting it
279
+ @self._settings_app.post("/validate_api_key")
280
+ async def _validate_key(payload: ApiKeyPayload) -> JSONResponse:
281
+ key = (payload.openai_api_key or "").strip()
282
+ if not key:
283
+ return JSONResponse({"valid": False, "error": "empty_key"}, status_code=400)
284
+
285
+ # Try to validate by checking if we can fetch the models
286
+ try:
287
+ import httpx
288
+ headers = {
289
+ "Authorization": f"Bearer {key}",
290
+ "Content-Type": "application/json"
291
+ }
292
+ async with httpx.AsyncClient(timeout=10.0) as client:
293
+ response = await client.get(
294
+ "https://api.openai.com/v1/models",
295
+ headers=headers
296
+ )
297
+ if response.status_code == 200:
298
+ return JSONResponse({"valid": True})
299
+ elif response.status_code == 401:
300
+ return JSONResponse({"valid": False, "error": "invalid_api_key"}, status_code=401)
301
+ else:
302
+ return JSONResponse({"valid": False, "error": "validation_failed"}, status_code=response.status_code)
303
+ except Exception as e:
304
+ logger.warning(f"API key validation failed: {e}")
305
+ return JSONResponse({"valid": False, "error": "validation_error"}, status_code=500)
306
+
307
  self._settings_initialized = True
308
 
309
  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", {
@@ -173,6 +187,7 @@ async function init() {
173
  const configuredPanel = document.getElementById("configured");
174
  const personalityPanel = document.getElementById("personality-panel");
175
  const saveBtn = document.getElementById("save-btn");
 
176
  const input = document.getElementById("api-key");
177
 
178
  // Personality elements
@@ -205,22 +220,55 @@ async function init() {
205
  show(configuredPanel, true);
206
  }
207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  saveBtn.addEventListener("click", async () => {
209
  const key = input.value.trim();
210
  if (!key) {
211
  statusEl.textContent = "Please enter a valid key.";
212
  statusEl.className = "status warn";
 
213
  return;
214
  }
215
- statusEl.textContent = "Saving...";
216
  statusEl.className = "status";
 
217
  try {
 
 
 
 
 
 
 
 
 
 
 
 
218
  await saveKey(key);
219
  statusEl.textContent = "Saved. Reloading…";
220
  statusEl.className = "status ok";
221
  window.location.reload();
222
  } catch (e) {
223
- statusEl.textContent = "Failed to save key.";
 
 
 
 
 
224
  statusEl.className = "status error";
225
  }
226
  });
 
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", {
 
187
  const configuredPanel = document.getElementById("configured");
188
  const personalityPanel = document.getElementById("personality-panel");
189
  const saveBtn = document.getElementById("save-btn");
190
+ const changeKeyBtn = document.getElementById("change-key-btn");
191
  const input = document.getElementById("api-key");
192
 
193
  // Personality elements
 
220
  show(configuredPanel, true);
221
  }
222
 
223
+ // Handler for "Change API key" button
224
+ changeKeyBtn.addEventListener("click", () => {
225
+ show(configuredPanel, false);
226
+ show(formPanel, true);
227
+ input.value = "";
228
+ statusEl.textContent = "";
229
+ statusEl.className = "status";
230
+ });
231
+
232
+ // Remove error styling when user starts typing
233
+ input.addEventListener("input", () => {
234
+ input.classList.remove("error");
235
+ });
236
+
237
  saveBtn.addEventListener("click", async () => {
238
  const key = input.value.trim();
239
  if (!key) {
240
  statusEl.textContent = "Please enter a valid key.";
241
  statusEl.className = "status warn";
242
+ input.classList.add("error");
243
  return;
244
  }
245
+ statusEl.textContent = "Validating API key...";
246
  statusEl.className = "status";
247
+ input.classList.remove("error");
248
  try {
249
+ // First validate the key
250
+ const validation = await validateKey(key);
251
+ if (!validation.valid) {
252
+ statusEl.textContent = "Invalid API key. Please check your key and try again.";
253
+ statusEl.className = "status error";
254
+ input.classList.add("error");
255
+ return;
256
+ }
257
+
258
+ // If valid, save it
259
+ statusEl.textContent = "Key valid! Saving...";
260
+ statusEl.className = "status ok";
261
  await saveKey(key);
262
  statusEl.textContent = "Saved. Reloading…";
263
  statusEl.className = "status ok";
264
  window.location.reload();
265
  } catch (e) {
266
+ input.classList.add("error");
267
+ if (e.message === "invalid_api_key") {
268
+ statusEl.textContent = "Invalid API key. Please check your key and try again.";
269
+ } else {
270
+ statusEl.textContent = "Failed to validate/save key. Please try again.";
271
+ }
272
  statusEl.className = "status error";
273
  }
274
  });
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);