relmio 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +42 -429
- package/docs/faq.md +45 -0
- package/docs/getting-started.md +38 -0
- package/docs/local-endpoints-spec.md +27 -0
- package/docs/local-endpoints.md +52 -11
- package/docs/reference.md +77 -0
- package/docs/security.md +34 -0
- package/docs/troubleshooting.md +33 -0
- package/docs/vps-and-n8n.md +30 -0
- package/package.json +1 -1
- package/src/services/local-chat-test.js +361 -0
- package/src/ui/local.css +115 -1
- package/src/ui/local.html +99 -0
- package/src/ui/local.js +263 -1
- package/src/web/server.js +123 -1
package/src/ui/local.css
CHANGED
|
@@ -365,6 +365,115 @@
|
|
|
365
365
|
font-weight: 700;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
.chat-tester {
|
|
369
|
+
display: grid;
|
|
370
|
+
gap: 1rem;
|
|
371
|
+
margin-top: 1.25rem;
|
|
372
|
+
padding: 1rem;
|
|
373
|
+
border: 1px solid var(--accent-border);
|
|
374
|
+
border-radius: var(--radius);
|
|
375
|
+
background: var(--accent-soft);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.chat-tester-heading {
|
|
379
|
+
display: flex;
|
|
380
|
+
align-items: flex-start;
|
|
381
|
+
justify-content: space-between;
|
|
382
|
+
gap: 1rem;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.chat-tester-heading h3 {
|
|
386
|
+
margin: 0.2rem 0 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.chat-tester-heading p:not(.section-label) {
|
|
390
|
+
margin: 0.45rem 0 0;
|
|
391
|
+
color: var(--text-muted);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.chat-tester-heading .section-label {
|
|
395
|
+
margin: 0;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.chat-tester-form fieldset {
|
|
399
|
+
display: grid;
|
|
400
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
401
|
+
gap: 0.875rem;
|
|
402
|
+
margin: 0;
|
|
403
|
+
padding: 0;
|
|
404
|
+
border: 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.chat-tester-form legend {
|
|
408
|
+
grid-column: 1 / -1;
|
|
409
|
+
width: 100%;
|
|
410
|
+
margin-bottom: 0.5rem;
|
|
411
|
+
font-weight: 800;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.chat-tester-form .button,
|
|
415
|
+
.chat-tester-message-form .button {
|
|
416
|
+
margin-top: 0.875rem;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.chat-tester-status,
|
|
420
|
+
.chat-tester-error {
|
|
421
|
+
margin: 0;
|
|
422
|
+
padding: 0.75rem 0.875rem;
|
|
423
|
+
border-radius: var(--radius-sm);
|
|
424
|
+
font-size: 0.875rem;
|
|
425
|
+
overflow-wrap: anywhere;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.chat-tester-status {
|
|
429
|
+
border: 1px solid var(--accent-border);
|
|
430
|
+
background: var(--surface);
|
|
431
|
+
color: var(--text-muted);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.chat-tester-error {
|
|
435
|
+
border: 1px solid var(--warning-border);
|
|
436
|
+
background: var(--warning-soft);
|
|
437
|
+
color: var(--warning);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.chat-tester-transcript {
|
|
441
|
+
display: grid;
|
|
442
|
+
gap: 0.75rem;
|
|
443
|
+
max-height: 28rem;
|
|
444
|
+
margin: 0;
|
|
445
|
+
padding: 0;
|
|
446
|
+
overflow: auto;
|
|
447
|
+
list-style: none;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.chat-tester-turn {
|
|
451
|
+
max-width: min(100%, 46rem);
|
|
452
|
+
padding: 0.75rem 0.875rem;
|
|
453
|
+
border: 1px solid var(--border-strong);
|
|
454
|
+
border-radius: var(--radius-sm);
|
|
455
|
+
background: var(--surface);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.chat-tester-turn-assistant {
|
|
459
|
+
border-color: var(--accent-border);
|
|
460
|
+
background: var(--accent-soft);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.chat-tester-turn strong {
|
|
464
|
+
display: block;
|
|
465
|
+
margin-bottom: 0.25rem;
|
|
466
|
+
font-size: 0.75rem;
|
|
467
|
+
letter-spacing: 0.02em;
|
|
468
|
+
text-transform: uppercase;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.chat-tester-turn p {
|
|
472
|
+
margin: 0;
|
|
473
|
+
overflow-wrap: anywhere;
|
|
474
|
+
white-space: pre-wrap;
|
|
475
|
+
}
|
|
476
|
+
|
|
368
477
|
.local-wizard a.button {
|
|
369
478
|
text-decoration: none;
|
|
370
479
|
}
|
|
@@ -379,10 +488,15 @@
|
|
|
379
488
|
@media (max-width: 48rem) {
|
|
380
489
|
.target-picker,
|
|
381
490
|
.local-fields,
|
|
382
|
-
.local-review-grid
|
|
491
|
+
.local-review-grid,
|
|
492
|
+
.chat-tester-form fieldset {
|
|
383
493
|
grid-template-columns: minmax(0, 1fr);
|
|
384
494
|
}
|
|
385
495
|
|
|
496
|
+
.chat-tester-heading {
|
|
497
|
+
flex-direction: column;
|
|
498
|
+
}
|
|
499
|
+
|
|
386
500
|
.review-details div {
|
|
387
501
|
grid-template-columns: minmax(0, 1fr);
|
|
388
502
|
gap: 0.2rem;
|
package/src/ui/local.html
CHANGED
|
@@ -142,6 +142,13 @@
|
|
|
142
142
|
hidden
|
|
143
143
|
>
|
|
144
144
|
<span id="global-error-text"></span>
|
|
145
|
+
<a
|
|
146
|
+
id="local-image-build-troubleshooting"
|
|
147
|
+
href="https://relmio.vercel.app/docs/troubleshooting#local-image-build-failed"
|
|
148
|
+
target="_blank"
|
|
149
|
+
rel="noopener noreferrer"
|
|
150
|
+
hidden
|
|
151
|
+
>View troubleshooting</a>
|
|
145
152
|
</div>
|
|
146
153
|
</div>
|
|
147
154
|
</aside>
|
|
@@ -504,6 +511,98 @@
|
|
|
504
511
|
</div>
|
|
505
512
|
</section>
|
|
506
513
|
|
|
514
|
+
<section
|
|
515
|
+
id="chat-tester"
|
|
516
|
+
class="chat-tester"
|
|
517
|
+
aria-labelledby="chat-tester-title"
|
|
518
|
+
hidden
|
|
519
|
+
>
|
|
520
|
+
<div class="chat-tester-heading">
|
|
521
|
+
<div>
|
|
522
|
+
<p class="section-label">Local-only tester</p>
|
|
523
|
+
<h3 id="chat-tester-title">Test this local Chat Adapter</h3>
|
|
524
|
+
<p>
|
|
525
|
+
Relmio sends this browser only to its same-origin local wizard.
|
|
526
|
+
The wizard then calls the literal loopback adapter address.
|
|
527
|
+
</p>
|
|
528
|
+
</div>
|
|
529
|
+
<button
|
|
530
|
+
id="chat-tester-reset"
|
|
531
|
+
class="button secondary"
|
|
532
|
+
type="button"
|
|
533
|
+
>
|
|
534
|
+
Forget tester
|
|
535
|
+
</button>
|
|
536
|
+
</div>
|
|
537
|
+
|
|
538
|
+
<form id="chat-tester-secure-form" class="chat-tester-form">
|
|
539
|
+
<fieldset>
|
|
540
|
+
<legend>Secure a temporary test session</legend>
|
|
541
|
+
<label class="field">
|
|
542
|
+
<span>Adapter base URL</span>
|
|
543
|
+
<input
|
|
544
|
+
id="chat-tester-endpoint"
|
|
545
|
+
type="url"
|
|
546
|
+
inputmode="url"
|
|
547
|
+
autocomplete="off"
|
|
548
|
+
spellcheck="false"
|
|
549
|
+
maxlength="64"
|
|
550
|
+
placeholder="http://127.0.0.1:14501"
|
|
551
|
+
pattern="http:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}\/?"
|
|
552
|
+
required
|
|
553
|
+
/>
|
|
554
|
+
<small>Literal <code>http://127.0.0.1:PORT</code> only.</small>
|
|
555
|
+
</label>
|
|
556
|
+
<label class="field">
|
|
557
|
+
<span>Client credential</span>
|
|
558
|
+
<input
|
|
559
|
+
id="chat-tester-credential"
|
|
560
|
+
type="password"
|
|
561
|
+
autocomplete="off"
|
|
562
|
+
autocapitalize="none"
|
|
563
|
+
spellcheck="false"
|
|
564
|
+
maxlength="512"
|
|
565
|
+
required
|
|
566
|
+
/>
|
|
567
|
+
<small id="chat-tester-security-note">
|
|
568
|
+
The field is cleared as soon as you secure it. Encryption prevents accidental transit/storage exposure but not a compromised browser, extension, or local machine.
|
|
569
|
+
</small>
|
|
570
|
+
</label>
|
|
571
|
+
</fieldset>
|
|
572
|
+
<button id="chat-tester-secure" class="button primary" type="submit">
|
|
573
|
+
Secure credential for this test
|
|
574
|
+
</button>
|
|
575
|
+
</form>
|
|
576
|
+
|
|
577
|
+
<form id="chat-tester-message-form" class="chat-tester-message-form" hidden>
|
|
578
|
+
<label class="field" for="chat-tester-input">
|
|
579
|
+
<span>Message</span>
|
|
580
|
+
<textarea
|
|
581
|
+
id="chat-tester-input"
|
|
582
|
+
rows="3"
|
|
583
|
+
maxlength="8192"
|
|
584
|
+
spellcheck="true"
|
|
585
|
+
required
|
|
586
|
+
></textarea>
|
|
587
|
+
</label>
|
|
588
|
+
<button id="chat-tester-send" class="button primary" type="submit">
|
|
589
|
+
Send test message
|
|
590
|
+
</button>
|
|
591
|
+
</form>
|
|
592
|
+
|
|
593
|
+
<p id="chat-tester-status" class="chat-tester-status" role="status" aria-live="polite">
|
|
594
|
+
Secure the local client credential to begin.
|
|
595
|
+
</p>
|
|
596
|
+
<p id="chat-tester-error" class="chat-tester-error" role="alert" hidden></p>
|
|
597
|
+
<ol
|
|
598
|
+
id="chat-tester-transcript"
|
|
599
|
+
class="chat-tester-transcript"
|
|
600
|
+
role="log"
|
|
601
|
+
aria-live="polite"
|
|
602
|
+
aria-label="Local adapter test transcript"
|
|
603
|
+
></ol>
|
|
604
|
+
</section>
|
|
605
|
+
|
|
507
606
|
<aside id="client-warning" class="final-note"></aside>
|
|
508
607
|
|
|
509
608
|
<div class="actions">
|
package/src/ui/local.js
CHANGED
|
@@ -10,6 +10,14 @@ const state = {
|
|
|
10
10
|
planId: null,
|
|
11
11
|
plan: null,
|
|
12
12
|
installedTarget: null,
|
|
13
|
+
chatTester: {
|
|
14
|
+
conversationId: null,
|
|
15
|
+
encryptedCredential: null,
|
|
16
|
+
endpointBaseUrl: null,
|
|
17
|
+
expiresAt: null,
|
|
18
|
+
generation: 0,
|
|
19
|
+
keyId: null,
|
|
20
|
+
},
|
|
13
21
|
};
|
|
14
22
|
|
|
15
23
|
const messageBox = element("global-message");
|
|
@@ -31,11 +39,16 @@ function setMessage(text) {
|
|
|
31
39
|
|
|
32
40
|
function clearError() {
|
|
33
41
|
errorText.textContent = "";
|
|
42
|
+
element("local-image-build-troubleshooting").hidden = true;
|
|
34
43
|
errorBox.hidden = true;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
function showError(error) {
|
|
38
|
-
|
|
47
|
+
const localImageBuildFailed = error?.message === "Local image build failed.";
|
|
48
|
+
errorText.textContent = localImageBuildFailed
|
|
49
|
+
? "Relmio could not build the local image. Check that Docker is running, has enough disk space, and can pull its base image."
|
|
50
|
+
: error?.message ?? "Something went wrong.";
|
|
51
|
+
element("local-image-build-troubleshooting").hidden = !localImageBuildFailed;
|
|
39
52
|
errorBox.hidden = false;
|
|
40
53
|
errorBox.focus();
|
|
41
54
|
}
|
|
@@ -269,6 +282,10 @@ function renderInstallResult(result) {
|
|
|
269
282
|
element("result-credential").textContent = result.clientCredential;
|
|
270
283
|
element("codex-production-warning").hidden = openAiApi;
|
|
271
284
|
element("codex-login").hidden = openAiApi;
|
|
285
|
+
element("chat-tester").hidden = !codexChat;
|
|
286
|
+
if (codexChat && !state.chatTester.keyId) {
|
|
287
|
+
element("chat-tester-endpoint").value = result.endpoint;
|
|
288
|
+
}
|
|
272
289
|
element("codex-production-warning-title").textContent = codexChat
|
|
273
290
|
? "Experimental Chat Adapter — trusted local backends or development servers only"
|
|
274
291
|
: "Experimental WebSocket transport";
|
|
@@ -300,6 +317,114 @@ function renderInstallResult(result) {
|
|
|
300
317
|
);
|
|
301
318
|
}
|
|
302
319
|
|
|
320
|
+
function setChatTesterStatus(text) {
|
|
321
|
+
element("chat-tester-status").textContent = text;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function clearChatTesterError() {
|
|
325
|
+
element("chat-tester-error").textContent = "";
|
|
326
|
+
element("chat-tester-error").hidden = true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function showChatTesterError(error) {
|
|
330
|
+
element("chat-tester-error").textContent =
|
|
331
|
+
error?.message ?? "The local adapter test could not be completed.";
|
|
332
|
+
element("chat-tester-error").hidden = false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function appendChatTesterTurn(kind, text) {
|
|
336
|
+
const item = document.createElement("li");
|
|
337
|
+
const heading = document.createElement("strong");
|
|
338
|
+
const content = document.createElement("p");
|
|
339
|
+
heading.textContent = kind === "user" ? "You" : "Local adapter";
|
|
340
|
+
content.textContent = text;
|
|
341
|
+
item.className = `chat-tester-turn chat-tester-turn-${kind}`;
|
|
342
|
+
item.append(heading, content);
|
|
343
|
+
element("chat-tester-transcript").append(item);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function clearChatTesterState() {
|
|
347
|
+
state.chatTester.conversationId = null;
|
|
348
|
+
state.chatTester.encryptedCredential = null;
|
|
349
|
+
state.chatTester.endpointBaseUrl = null;
|
|
350
|
+
state.chatTester.expiresAt = null;
|
|
351
|
+
state.chatTester.keyId = null;
|
|
352
|
+
state.chatTester.generation += 1;
|
|
353
|
+
element("chat-tester-credential").value = "";
|
|
354
|
+
element("chat-tester-endpoint").value = "";
|
|
355
|
+
element("chat-tester-input").value = "";
|
|
356
|
+
element("chat-tester-secure-form").hidden = false;
|
|
357
|
+
element("chat-tester-message-form").hidden = true;
|
|
358
|
+
element("chat-tester-transcript").replaceChildren();
|
|
359
|
+
clearChatTesterError();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function assertChatTesterKey(result) {
|
|
363
|
+
if (
|
|
364
|
+
!result ||
|
|
365
|
+
typeof result !== "object" ||
|
|
366
|
+
typeof result.keyId !== "string" ||
|
|
367
|
+
typeof result.expiresAt !== "string" ||
|
|
368
|
+
result.algorithm !== "RSA-OAEP-256" ||
|
|
369
|
+
!result.publicKeyJwk ||
|
|
370
|
+
result.publicKeyJwk.kty !== "RSA" ||
|
|
371
|
+
typeof result.publicKeyJwk.n !== "string" ||
|
|
372
|
+
typeof result.publicKeyJwk.e !== "string"
|
|
373
|
+
) {
|
|
374
|
+
throw new Error("The local tester returned an unexpected encryption key.");
|
|
375
|
+
}
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function encryptChatTesterCredential(publicKeyJwk, clientCredential) {
|
|
380
|
+
if (!window.crypto?.subtle) {
|
|
381
|
+
throw new Error("This browser cannot create the required local test encryption key.");
|
|
382
|
+
}
|
|
383
|
+
const publicKey = await window.crypto.subtle.importKey(
|
|
384
|
+
"jwk",
|
|
385
|
+
publicKeyJwk,
|
|
386
|
+
{ name: "RSA-OAEP", hash: "SHA-256" },
|
|
387
|
+
false,
|
|
388
|
+
["encrypt"],
|
|
389
|
+
);
|
|
390
|
+
const encrypted = await window.crypto.subtle.encrypt(
|
|
391
|
+
{ name: "RSA-OAEP" },
|
|
392
|
+
publicKey,
|
|
393
|
+
new TextEncoder().encode(clientCredential),
|
|
394
|
+
);
|
|
395
|
+
const bytes = new Uint8Array(encrypted);
|
|
396
|
+
let binary = "";
|
|
397
|
+
for (const byte of bytes) {
|
|
398
|
+
binary += String.fromCodePoint(byte);
|
|
399
|
+
}
|
|
400
|
+
return window.btoa(binary);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function forgetChatTester({ announce = true } = {}) {
|
|
404
|
+
const keyId = state.chatTester.keyId;
|
|
405
|
+
clearChatTesterState();
|
|
406
|
+
if (!keyId) {
|
|
407
|
+
if (announce) {
|
|
408
|
+
setChatTesterStatus("The tester is cleared. Secure a credential to begin again.");
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
await api("/api/local/chat-test/reset", {
|
|
414
|
+
method: "POST",
|
|
415
|
+
body: { keyId },
|
|
416
|
+
});
|
|
417
|
+
if (announce) {
|
|
418
|
+
setChatTesterStatus("The tester key and transcript were forgotten.");
|
|
419
|
+
}
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (announce) {
|
|
422
|
+
showChatTesterError(error);
|
|
423
|
+
setChatTesterStatus("The browser tester was cleared. The local key will expire shortly.");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
303
428
|
function validateVerificationUrl(value) {
|
|
304
429
|
if (typeof value !== "string" || value.length > 2048) {
|
|
305
430
|
throw new Error("Relmio refused an unexpected sign-in destination.");
|
|
@@ -519,6 +644,9 @@ element("rotate-credential-button").addEventListener("click", async (event) => {
|
|
|
519
644
|
"Generating a replacement credential before activating it…",
|
|
520
645
|
);
|
|
521
646
|
try {
|
|
647
|
+
if (isCodexChat(state.installedTarget)) {
|
|
648
|
+
await forgetChatTester({ announce: false });
|
|
649
|
+
}
|
|
522
650
|
const staged = await api("/api/local/client-credential/rotate", {
|
|
523
651
|
method: "POST",
|
|
524
652
|
body: { target: state.installedTarget },
|
|
@@ -544,6 +672,140 @@ element("rotate-credential-button").addEventListener("click", async (event) => {
|
|
|
544
672
|
}
|
|
545
673
|
});
|
|
546
674
|
|
|
675
|
+
element("chat-tester-secure-form").addEventListener("submit", async (event) => {
|
|
676
|
+
event.preventDefault();
|
|
677
|
+
const form = event.currentTarget;
|
|
678
|
+
const button = element("chat-tester-secure");
|
|
679
|
+
const resetButton = element("chat-tester-reset");
|
|
680
|
+
const endpointInput = element("chat-tester-endpoint");
|
|
681
|
+
const clientCredentialInput = element("chat-tester-credential");
|
|
682
|
+
if (!form.reportValidity()) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
clearChatTesterError();
|
|
687
|
+
let clientCredential = clientCredentialInput.value;
|
|
688
|
+
clientCredentialInput.value = "";
|
|
689
|
+
let issuedKey;
|
|
690
|
+
resetButton.disabled = true;
|
|
691
|
+
setBusy(button, true, "Securing credential…");
|
|
692
|
+
setChatTesterStatus("Creating a short-lived local encryption key…");
|
|
693
|
+
try {
|
|
694
|
+
issuedKey = assertChatTesterKey(
|
|
695
|
+
await api("/api/local/chat-test/key", { method: "POST", body: {} }),
|
|
696
|
+
);
|
|
697
|
+
const encryptedCredential = await encryptChatTesterCredential(
|
|
698
|
+
issuedKey.publicKeyJwk,
|
|
699
|
+
clientCredential,
|
|
700
|
+
);
|
|
701
|
+
clientCredential = undefined;
|
|
702
|
+
state.chatTester.conversationId = null;
|
|
703
|
+
state.chatTester.encryptedCredential = encryptedCredential;
|
|
704
|
+
state.chatTester.endpointBaseUrl = endpointInput.value;
|
|
705
|
+
state.chatTester.expiresAt = issuedKey.expiresAt;
|
|
706
|
+
state.chatTester.keyId = issuedKey.keyId;
|
|
707
|
+
element("chat-tester-secure-form").hidden = true;
|
|
708
|
+
element("chat-tester-message-form").hidden = false;
|
|
709
|
+
setChatTesterStatus("Temporary test session secured. Send a message before the key expires.");
|
|
710
|
+
element("chat-tester-input").focus();
|
|
711
|
+
} catch (error) {
|
|
712
|
+
clientCredential = undefined;
|
|
713
|
+
if (issuedKey?.keyId) {
|
|
714
|
+
try {
|
|
715
|
+
await api("/api/local/chat-test/reset", {
|
|
716
|
+
method: "POST",
|
|
717
|
+
body: { keyId: issuedKey.keyId },
|
|
718
|
+
});
|
|
719
|
+
} catch {
|
|
720
|
+
// The only remaining server material is an expiring private key.
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
clearChatTesterState();
|
|
724
|
+
showChatTesterError(error);
|
|
725
|
+
setChatTesterStatus("The credential was cleared. Secure it again to retry.");
|
|
726
|
+
} finally {
|
|
727
|
+
clientCredential = undefined;
|
|
728
|
+
clientCredentialInput.value = "";
|
|
729
|
+
resetButton.disabled = false;
|
|
730
|
+
setBusy(button, false);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
element("chat-tester-message-form").addEventListener("submit", async (event) => {
|
|
735
|
+
event.preventDefault();
|
|
736
|
+
const form = event.currentTarget;
|
|
737
|
+
const button = element("chat-tester-send");
|
|
738
|
+
const resetButton = element("chat-tester-reset");
|
|
739
|
+
const input = element("chat-tester-input");
|
|
740
|
+
if (!form.reportValidity()) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (
|
|
744
|
+
!state.chatTester.keyId ||
|
|
745
|
+
!state.chatTester.encryptedCredential ||
|
|
746
|
+
!state.chatTester.endpointBaseUrl
|
|
747
|
+
) {
|
|
748
|
+
showChatTesterError(
|
|
749
|
+
new Error("This test credential has expired or was forgotten. Secure it again."),
|
|
750
|
+
);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
clearChatTesterError();
|
|
755
|
+
const text = input.value;
|
|
756
|
+
const generation = state.chatTester.generation;
|
|
757
|
+
input.value = "";
|
|
758
|
+
appendChatTesterTurn("user", text);
|
|
759
|
+
resetButton.disabled = true;
|
|
760
|
+
setBusy(button, true, "Waiting for adapter…");
|
|
761
|
+
setChatTesterStatus("The local wizard is testing the adapter without exposing its credential to the page.");
|
|
762
|
+
try {
|
|
763
|
+
const result = await api("/api/local/chat-test/message", {
|
|
764
|
+
method: "POST",
|
|
765
|
+
body: {
|
|
766
|
+
endpointBaseUrl: state.chatTester.endpointBaseUrl,
|
|
767
|
+
keyId: state.chatTester.keyId,
|
|
768
|
+
encryptedCredential: state.chatTester.encryptedCredential,
|
|
769
|
+
input: text,
|
|
770
|
+
...(state.chatTester.conversationId
|
|
771
|
+
? { conversationId: state.chatTester.conversationId }
|
|
772
|
+
: {}),
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
if (
|
|
776
|
+
typeof result.conversationId !== "string" ||
|
|
777
|
+
typeof result.output !== "string"
|
|
778
|
+
) {
|
|
779
|
+
throw new Error("The local adapter returned an unexpected response.");
|
|
780
|
+
}
|
|
781
|
+
if (state.chatTester.generation !== generation) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
state.chatTester.conversationId = result.conversationId;
|
|
785
|
+
appendChatTesterTurn("assistant", result.output);
|
|
786
|
+
setChatTesterStatus("Response received. Continue this conversation or forget the tester.");
|
|
787
|
+
} catch (error) {
|
|
788
|
+
if (state.chatTester.generation === generation) {
|
|
789
|
+
showChatTesterError(error);
|
|
790
|
+
setChatTesterStatus("No adapter response was added. You can retry or forget this tester.");
|
|
791
|
+
}
|
|
792
|
+
} finally {
|
|
793
|
+
resetButton.disabled = false;
|
|
794
|
+
setBusy(button, false);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
element("chat-tester-reset").addEventListener("click", async (event) => {
|
|
799
|
+
const button = event.currentTarget;
|
|
800
|
+
clearChatTesterError();
|
|
801
|
+
setBusy(button, true, "Forgetting tester…");
|
|
802
|
+
try {
|
|
803
|
+
await forgetChatTester();
|
|
804
|
+
} finally {
|
|
805
|
+
setBusy(button, false);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
547
809
|
element("codex-login-button").addEventListener("click", async (event) => {
|
|
548
810
|
const button = event.currentTarget;
|
|
549
811
|
const resultBox = element("device-code-result");
|