Skip to content

Commit dbaac71

Browse files
authored
fix(chat ui): fix ui inconsistencies and bugs (#15)
1 parent 577e7e1 commit dbaac71

File tree

8 files changed

+224
-89
lines changed

8 files changed

+224
-89
lines changed

sandboxes/nemoclaw/nemoclaw-start.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ openclaw onboard \
7979
--skip-health \
8080
--auth-choice custom-api-key \
8181
--custom-base-url "https://inference.local/v1" \
82-
--custom-model-id "aws/anthropic/bedrock-claude-opus-4-6" \
82+
--custom-model-id "-" \
8383
--custom-api-key "$_ONBOARD_KEY" \
8484
--secret-input-mode plaintext \
8585
--custom-compatibility openai \

sandboxes/nemoclaw/nemoclaw-ui-extension/extension/api-keys-page.ts

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* model-registry.ts getter functions.
77
*/
88

9-
import { ICON_KEY, ICON_EYE, ICON_EYE_OFF, ICON_CHECK } from "./icons.ts";
9+
import { ICON_KEY, ICON_EYE, ICON_EYE_OFF, ICON_CHECK, ICON_LOADER, ICON_CLOSE } from "./icons.ts";
1010
import {
1111
getInferenceApiKey,
1212
getIntegrateApiKey,
@@ -24,6 +24,7 @@ interface KeyFieldDef {
2424
label: string;
2525
description: string;
2626
placeholder: string;
27+
serverCredentialKey: string;
2728
get: () => string;
2829
set: (v: string) => void;
2930
}
@@ -34,6 +35,7 @@ const KEY_FIELDS: KeyFieldDef[] = [
3435
label: "Inference API Key",
3536
description: "For inference-api.nvidia.com — powers NVIDIA Claude Opus 4.6",
3637
placeholder: "nvapi-...",
38+
serverCredentialKey: "OPENAI_API_KEY",
3739
get: getInferenceApiKey,
3840
set: setInferenceApiKey,
3941
},
@@ -42,11 +44,67 @@ const KEY_FIELDS: KeyFieldDef[] = [
4244
label: "Integrate API Key",
4345
description: "For integrate.api.nvidia.com — powers Kimi K2.5, Nemotron Ultra, DeepSeek V3.2",
4446
placeholder: "nvapi-...",
47+
serverCredentialKey: "NVIDIA_API_KEY",
4548
get: getIntegrateApiKey,
4649
set: setIntegrateApiKey,
4750
},
4851
];
4952

53+
// ---------------------------------------------------------------------------
54+
// Sync localStorage keys to server-side provider credentials
55+
// ---------------------------------------------------------------------------
56+
57+
interface ProviderSummary {
58+
name: string;
59+
type: string;
60+
credentialKeys: string[];
61+
}
62+
63+
/**
64+
* Push localStorage API keys to every server-side provider whose credential
65+
* key matches. This bridges the gap between the browser-only API Keys tab
66+
* and the NemoClaw proxy which reads credentials from the server-side store.
67+
*/
68+
export async function syncKeysToProviders(): Promise<void> {
69+
const res = await fetch("/api/providers");
70+
if (!res.ok) throw new Error(`Failed to fetch providers: ${res.status}`);
71+
const body = await res.json();
72+
if (!body.ok) throw new Error(body.error || "Failed to fetch providers");
73+
74+
const providers: ProviderSummary[] = body.providers || [];
75+
const errors: string[] = [];
76+
77+
for (const provider of providers) {
78+
for (const field of KEY_FIELDS) {
79+
const key = field.get();
80+
if (!isKeyConfigured(key)) continue;
81+
if (!provider.credentialKeys?.includes(field.serverCredentialKey)) continue;
82+
83+
try {
84+
const updateRes = await fetch(`/api/providers/${encodeURIComponent(provider.name)}`, {
85+
method: "PUT",
86+
headers: { "Content-Type": "application/json" },
87+
body: JSON.stringify({
88+
type: provider.type,
89+
credentials: { [field.serverCredentialKey]: key },
90+
config: {},
91+
}),
92+
});
93+
const updateBody = await updateRes.json();
94+
if (!updateBody.ok) {
95+
errors.push(`${provider.name}: ${updateBody.error || "update failed"}`);
96+
}
97+
} catch (err) {
98+
errors.push(`${provider.name}: ${err}`);
99+
}
100+
}
101+
}
102+
103+
if (errors.length > 0) {
104+
throw new Error(errors.join("; "));
105+
}
106+
}
107+
50108
// ---------------------------------------------------------------------------
51109
// Render the API Keys page into a container element
52110
// ---------------------------------------------------------------------------
@@ -71,7 +129,7 @@ export function renderApiKeysPage(container: HTMLElement): void {
71129
Enter your NVIDIA API keys to enable model switching and DGX deployment.
72130
Keys are stored locally in your browser and never sent to third parties.
73131
</p>
74-
<a class="nemoclaw-key-intro__link" href="https://build.nvidia.com/models" target="_blank" rel="noopener noreferrer">
132+
<a class="nemoclaw-key-intro__link" href="https://build.nvidia.com/settings/api-keys" target="_blank" rel="noopener noreferrer">
75133
Get your keys at build.nvidia.com &rarr;
76134
</a>`;
77135
page.appendChild(intro);
@@ -100,20 +158,33 @@ export function renderApiKeysPage(container: HTMLElement): void {
100158
form.appendChild(actions);
101159
page.appendChild(form);
102160

103-
saveBtn.addEventListener("click", () => {
161+
saveBtn.addEventListener("click", async () => {
104162
for (const field of KEY_FIELDS) {
105163
const input = form.querySelector<HTMLInputElement>(`[data-key-id="${field.id}"]`);
106164
if (input) field.set(input.value.trim());
107165
}
108166

109167
updateStatusDots();
110168

111-
feedback.className = "nemoclaw-key-feedback nemoclaw-key-feedback--success";
112-
feedback.innerHTML = `${ICON_CHECK}<span>Keys saved</span>`;
113-
setTimeout(() => {
114-
feedback.className = "nemoclaw-key-feedback";
115-
feedback.textContent = "";
116-
}, 3000);
169+
feedback.className = "nemoclaw-key-feedback nemoclaw-key-feedback--saving";
170+
feedback.innerHTML = `${ICON_LOADER}<span>Syncing keys to providers\u2026</span>`;
171+
saveBtn.disabled = true;
172+
173+
try {
174+
await syncKeysToProviders();
175+
feedback.className = "nemoclaw-key-feedback nemoclaw-key-feedback--success";
176+
feedback.innerHTML = `${ICON_CHECK}<span>Keys saved &amp; synced to providers</span>`;
177+
} catch (err) {
178+
console.warn("[NeMoClaw] Provider key sync failed:", err);
179+
feedback.className = "nemoclaw-key-feedback nemoclaw-key-feedback--error";
180+
feedback.innerHTML = `${ICON_CLOSE}<span>Keys saved locally but sync failed</span>`;
181+
} finally {
182+
saveBtn.disabled = false;
183+
setTimeout(() => {
184+
feedback.className = "nemoclaw-key-feedback";
185+
feedback.textContent = "";
186+
}, 4000);
187+
}
117188
});
118189
}
119190

sandboxes/nemoclaw/nemoclaw-ui-extension/extension/index.ts

Lines changed: 48 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* 1. A green "Deploy DGX Spark/Station" CTA button in the topbar
66
* 2. A "NeMoClaw" collapsible nav group with Policy, Inference Routes,
77
* and API Keys pages
8-
* 3. A model selector wired to NVIDIA endpoints via config.patch
8+
* 3. A model selector wired to NVIDIA endpoints
99
*
1010
* Operates purely as an overlay — no original OpenClaw source files are modified.
1111
*/
@@ -15,7 +15,8 @@ import { injectButton } from "./deploy-modal.ts";
1515
import { injectNavGroup, activateNemoPage, watchOpenClawNavClicks } from "./nav-group.ts";
1616
import { injectModelSelector, watchChatCompose } from "./model-selector.ts";
1717
import { ingestKeysFromUrl, DEFAULT_MODEL, resolveApiKey, isKeyConfigured } from "./model-registry.ts";
18-
import { waitForClient, patchConfig, waitForReconnect } from "./gateway-bridge.ts";
18+
import { waitForClient, waitForReconnect, patchConfig } from "./gateway-bridge.ts";
19+
import { syncKeysToProviders } from "./api-keys-page.ts";
1920

2021
function inject(): boolean {
2122
const hasButton = injectButton();
@@ -38,50 +39,6 @@ function watchGotoLinks() {
3839
});
3940
}
4041

41-
/**
42-
* Update the NemoClaw provider credential on the host so the sandbox
43-
* proxy / inference router uses the real key for inference.local requests.
44-
* Mirrors the policy-sync pattern in policy-page.ts.
45-
*/
46-
function injectKeyViaHost(key: string): void {
47-
fetch("/api/inject-key", {
48-
method: "POST",
49-
headers: { "Content-Type": "application/json" },
50-
body: JSON.stringify({ key }),
51-
})
52-
.then((r) => r.json())
53-
.then((b) => console.log("[NeMoClaw] inject-key:", b))
54-
.catch((e) => console.warn("[NeMoClaw] inject-key failed:", e));
55-
}
56-
57-
/**
58-
* When API keys arrive via URL parameters (from the welcome UI), apply
59-
* the default model's provider config so the gateway has a valid key
60-
* immediately rather than the placeholder set during onboarding.
61-
*/
62-
function applyIngestedKeys(): void {
63-
waitForClient().then(async () => {
64-
const apiKey = resolveApiKey(DEFAULT_MODEL.keyType);
65-
await patchConfig({
66-
models: {
67-
providers: {
68-
[DEFAULT_MODEL.providerKey]: {
69-
baseUrl: DEFAULT_MODEL.providerConfig.baseUrl,
70-
api: DEFAULT_MODEL.providerConfig.api,
71-
models: DEFAULT_MODEL.providerConfig.models,
72-
apiKey,
73-
},
74-
},
75-
},
76-
agents: {
77-
defaults: { model: { primary: DEFAULT_MODEL.modelRef } },
78-
},
79-
});
80-
}).catch((err) => {
81-
console.error("[NeMoClaw] Failed to apply ingested API key:", err);
82-
});
83-
}
84-
8542
/**
8643
* Insert a full-screen loading overlay that covers the OpenClaw UI while the
8744
* gateway connects and auto-pairs the device. The overlay is styled via
@@ -108,10 +65,51 @@ function revealApp(): void {
10865
}
10966
}
11067

68+
/**
69+
* Read the live OpenClaw config, find the active model.primary ref, and
70+
* patch streaming: true for it. For proxy-managed models the model.primary
71+
* never changes after onboard, so enabling it once covers every proxy model
72+
* switch.
73+
*/
74+
async function enableStreamingForActiveModel(): Promise<void> {
75+
const client = await waitForClient();
76+
const snapshot = await client.request<Record<string, unknown>>("config.get", {});
77+
78+
const agents = snapshot?.agents as Record<string, unknown> | undefined;
79+
const defaults = agents?.defaults as Record<string, unknown> | undefined;
80+
const model = defaults?.model as Record<string, unknown> | undefined;
81+
const primary = model?.primary as string | undefined;
82+
83+
if (!primary) {
84+
console.warn("[NeMoClaw] Could not determine active model primary from config");
85+
return;
86+
}
87+
88+
const models = defaults?.models as Record<string, Record<string, unknown>> | undefined;
89+
if (models?.[primary]?.streaming === true) return;
90+
91+
await patchConfig({
92+
agents: {
93+
defaults: {
94+
models: {
95+
[primary]: { streaming: true },
96+
},
97+
},
98+
},
99+
});
100+
}
101+
111102
function bootstrap() {
112103
showConnectOverlay();
113104

114-
waitForReconnect(30_000).then(revealApp).catch(revealApp);
105+
waitForReconnect(30_000)
106+
.then(() => {
107+
revealApp();
108+
enableStreamingForActiveModel().catch((err) =>
109+
console.warn("[NeMoClaw] Failed to enable streaming:", err),
110+
);
111+
})
112+
.catch(revealApp);
115113

116114
const keysIngested = ingestKeysFromUrl();
117115

@@ -121,8 +119,9 @@ function bootstrap() {
121119

122120
const defaultKey = resolveApiKey(DEFAULT_MODEL.keyType);
123121
if (keysIngested || isKeyConfigured(defaultKey)) {
124-
applyIngestedKeys();
125-
injectKeyViaHost(defaultKey);
122+
syncKeysToProviders().catch((e) =>
123+
console.warn("[NeMoClaw] bootstrap provider key sync failed:", e),
124+
);
126125
}
127126

128127
if (inject()) {

sandboxes/nemoclaw/nemoclaw-ui-extension/extension/inference-page.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ function buildQuickChip(modelId: string, name: string, providerName: string, cur
379379
const items = getCustomQuickSelects().filter((i) => i.modelId !== modelId);
380380
saveCustomQuickSelects(items);
381381
chip.remove();
382+
refreshModelSelector().catch(() => {});
382383
});
383384
chip.appendChild(removeBtn);
384385
}
@@ -439,6 +440,7 @@ function showAddQuickSelectForm(section: HTMLElement): void {
439440
saveCustomQuickSelects(items);
440441
form.remove();
441442
rerenderQuickPicker(section);
443+
refreshModelSelector().catch(() => {});
442444
});
443445

444446
btns.appendChild(addConfirm);

0 commit comments

Comments
 (0)