Skip to content

Commit 1189b37

Browse files
committed
updates to make everything more robust, change/update when settings change.
1 parent 0311c59 commit 1189b37

6 files changed

Lines changed: 73 additions & 135 deletions

File tree

frontend/app/aipanel/ai-utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,8 @@ export const getFilteredAIModeConfigs = (
547547
aiModeConfigs: Record<string, AIModeConfigType>,
548548
showCloudModes: boolean,
549549
inBuilder: boolean,
550-
hasPremium: boolean
550+
hasPremium: boolean,
551+
currentMode?: string
551552
): FilteredAIModeConfigs => {
552553
const hideQuick = inBuilder && hasPremium;
553554

@@ -560,7 +561,8 @@ export const getFilteredAIModeConfigs = (
560561
.sort(sortByDisplayOrder);
561562

562563
const hasCustomModels = otherProviderConfigs.length > 0;
563-
const shouldShowCloudModes = showCloudModes || !hasCustomModels;
564+
const isCurrentModeCloud = currentMode?.startsWith("waveai@") ?? false;
565+
const shouldShowCloudModes = showCloudModes || !hasCustomModels || isCurrentModeCloud;
564566

565567
const waveProviderConfigs = shouldShowCloudModes
566568
? allConfigs.filter((config) => config["ai:provider"] === "wave").sort(sortByDisplayOrder)

frontend/app/aipanel/aimode.tsx

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function computeWaveCloudSections(
121121
sections.push({
122122
sectionName: "Wave AI Cloud",
123123
configs: waveProviderConfigs,
124-
noTelemetry: !telemetryEnabled
124+
noTelemetry: !telemetryEnabled,
125125
});
126126
}
127127
if (otherProviderConfigs.length > 0) {
@@ -137,13 +137,12 @@ interface AIModeDropdownProps {
137137

138138
export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdownProps) => {
139139
const model = WaveAIModel.getInstance();
140-
const aiMode = useAtomValue(model.currentAIMode);
140+
const currentMode = useAtomValue(model.currentAIMode);
141141
const aiModeConfigs = useAtomValue(model.aiModeConfigs);
142142
const waveaiModeConfigs = useAtomValue(atoms.waveaiModeConfigAtom);
143143
const widgetContextEnabled = useAtomValue(model.widgetAccessAtom);
144144
const hasPremium = useAtomValue(model.hasPremiumAtom);
145145
const showCloudModes = useAtomValue(getSettingsKeyAtom("waveai:showcloudmodes"));
146-
const defaultMode = useAtomValue(getSettingsKeyAtom("waveai:defaultmode")) ?? "waveai@balanced";
147146
const telemetryEnabled = useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false;
148147
const [isOpen, setIsOpen] = useState(false);
149148
const dropdownRef = useRef<HTMLDivElement>(null);
@@ -152,20 +151,10 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
152151
aiModeConfigs,
153152
showCloudModes,
154153
model.inBuilder,
155-
hasPremium
154+
hasPremium,
155+
currentMode
156156
);
157157

158-
let currentMode = aiMode || defaultMode;
159-
const currentConfig = aiModeConfigs[currentMode];
160-
if (currentConfig) {
161-
if (!hasPremium && currentConfig["waveai:premium"]) {
162-
currentMode = "waveai@quick";
163-
}
164-
if (model.inBuilder && hasPremium && currentMode === "waveai@quick") {
165-
currentMode = "waveai@balanced";
166-
}
167-
}
168-
169158
const sections: ConfigSection[] = compatibilityMode
170159
? computeCompatibleSections(currentMode, aiModeConfigs, waveProviderConfigs, otherProviderConfigs)
171160
: computeWaveCloudSections(waveProviderConfigs, otherProviderConfigs, telemetryEnabled);
@@ -183,12 +172,17 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
183172
};
184173

185174
const displayConfig = aiModeConfigs[currentMode];
186-
const displayName = displayConfig ? getModeDisplayName(displayConfig) : "Unknown";
187-
const displayIcon = displayConfig?.["display:icon"] || "sparkles";
175+
const displayName = displayConfig ? getModeDisplayName(displayConfig) : `Invalid (${currentMode})`;
176+
const displayIcon = displayConfig ? (displayConfig["display:icon"] || "sparkles") : "question";
188177
const resolvedConfig = waveaiModeConfigs[currentMode];
189178
const hasToolsSupport = resolvedConfig && resolvedConfig["ai:capabilities"]?.includes("tools");
190179
const showNoToolsWarning = widgetContextEnabled && resolvedConfig && !hasToolsSupport;
191180

181+
const handleNewChatClick = () => {
182+
model.clearChat();
183+
setIsOpen(false);
184+
};
185+
192186
const handleConfigureClick = () => {
193187
fireAndForget(async () => {
194188
RpcApi.RecordTEventCommand(
@@ -278,7 +272,8 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
278272
const isPremiumDisabled = !hasPremium && config["waveai:premium"];
279273
const isIncompatibleDisabled = section.isIncompatible || false;
280274
const isTelemetryDisabled = section.noTelemetry || false;
281-
const isDisabled = isPremiumDisabled || isIncompatibleDisabled || isTelemetryDisabled;
275+
const isDisabled =
276+
isPremiumDisabled || isIncompatibleDisabled || isTelemetryDisabled;
282277
const isSelected = currentMode === config.mode;
283278
return (
284279
<AIModeMenuItem
@@ -296,6 +291,13 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
296291
);
297292
})}
298293
<div className="border-t border-gray-600 my-1" />
294+
<button
295+
onClick={handleNewChatClick}
296+
className="w-full flex items-center gap-2 px-3 pt-1 pb-1 text-gray-300 hover:bg-zinc-700 cursor-pointer transition-colors text-left"
297+
>
298+
<i className={makeIconClass("plus", false)}></i>
299+
<span className="text-sm">New Chat</span>
300+
</button>
299301
<button
300302
onClick={handleConfigureClick}
301303
className="w-full flex items-center gap-2 px-3 pt-1 pb-2 text-gray-300 hover:bg-zinc-700 cursor-pointer transition-colors text-left"

frontend/app/aipanel/aipanel-contextmenu.ts

Lines changed: 1 addition & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { getFilteredAIModeConfigs, getModeDisplayName } from "@/app/aipanel/ai-utils";
54
import { waveAIHasSelection } from "@/app/aipanel/waveai-focus-utils";
65
import { ContextMenuModel } from "@/app/store/contextmenu";
7-
import { atoms, getSettingsKeyAtom, isDev } from "@/app/store/global";
6+
import { isDev } from "@/app/store/global";
87
import { globalStore } from "@/app/store/jotaiStore";
98
import { RpcApi } from "@/app/store/wshclientapi";
109
import { TabRpcClient } from "@/app/store/wshrpcutil";
@@ -40,78 +39,9 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo
4039
oref: model.orefContext,
4140
});
4241

43-
const hasPremium = globalStore.get(model.hasPremiumAtom);
44-
const aiModeConfigs = globalStore.get(model.aiModeConfigs);
45-
const showCloudModes = globalStore.get(getSettingsKeyAtom("waveai:showcloudmodes"));
46-
const currentAIMode = rtInfo?.["waveai:mode"] ?? (hasPremium ? "waveai@balanced" : "waveai@quick");
4742
const defaultTokens = model.inBuilder ? 24576 : 4096;
4843
const currentMaxTokens = rtInfo?.["waveai:maxoutputtokens"] ?? defaultTokens;
4944

50-
const { waveProviderConfigs, otherProviderConfigs } = getFilteredAIModeConfigs(
51-
aiModeConfigs,
52-
showCloudModes,
53-
model.inBuilder,
54-
hasPremium
55-
);
56-
57-
const aiModeSubmenu: ContextMenuItem[] = [];
58-
59-
if (waveProviderConfigs.length > 0) {
60-
aiModeSubmenu.push({
61-
label: "Wave AI Modes",
62-
type: "header",
63-
enabled: false,
64-
});
65-
66-
waveProviderConfigs.forEach(({ mode, ...config }) => {
67-
const isPremium = config["waveai:premium"] === true;
68-
const isEnabled = !isPremium || hasPremium;
69-
aiModeSubmenu.push({
70-
label: getModeDisplayName(config),
71-
type: "checkbox",
72-
checked: currentAIMode === mode,
73-
enabled: isEnabled,
74-
click: () => {
75-
if (!isEnabled) return;
76-
RpcApi.SetRTInfoCommand(TabRpcClient, {
77-
oref: model.orefContext,
78-
data: { "waveai:mode": mode },
79-
});
80-
},
81-
});
82-
});
83-
}
84-
85-
if (otherProviderConfigs.length > 0) {
86-
if (waveProviderConfigs.length > 0) {
87-
aiModeSubmenu.push({ type: "separator" });
88-
}
89-
90-
aiModeSubmenu.push({
91-
label: "Custom Modes",
92-
type: "header",
93-
enabled: false,
94-
});
95-
96-
otherProviderConfigs.forEach(({ mode, ...config }) => {
97-
const isPremium = config["waveai:premium"] === true;
98-
const isEnabled = !isPremium || hasPremium;
99-
aiModeSubmenu.push({
100-
label: getModeDisplayName(config),
101-
type: "checkbox",
102-
checked: currentAIMode === mode,
103-
enabled: isEnabled,
104-
click: () => {
105-
if (!isEnabled) return;
106-
RpcApi.SetRTInfoCommand(TabRpcClient, {
107-
oref: model.orefContext,
108-
data: { "waveai:mode": mode },
109-
});
110-
},
111-
});
112-
});
113-
}
114-
11545
const maxTokensSubmenu: ContextMenuItem[] = [];
11646

11747
if (model.inBuilder) {
@@ -190,11 +120,6 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo
190120
);
191121
}
192122

193-
menu.push({
194-
label: "AI Mode",
195-
submenu: aiModeSubmenu,
196-
});
197-
198123
menu.push({
199124
label: "Max Output Tokens",
200125
submenu: maxTokensSubmenu,

frontend/app/aipanel/aipanel.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -197,49 +197,58 @@ const AIBuilderWelcomeMessage = memo(() => {
197197

198198
AIBuilderWelcomeMessage.displayName = "AIBuilderWelcomeMessage";
199199

200-
interface AIErrorMessageProps {
201-
errorMessage: string;
202-
onClear: () => void;
203-
}
200+
const AIErrorMessage = memo(() => {
201+
const model = WaveAIModel.getInstance();
202+
const errorMessage = jotai.useAtomValue(model.errorMessage);
203+
204+
if (!errorMessage) {
205+
return null;
206+
}
204207

205-
const AIErrorMessage = memo(({ errorMessage, onClear }: AIErrorMessageProps) => {
206208
return (
207209
<div className="px-4 py-2 text-red-400 bg-red-900/20 border-l-4 border-red-500 mx-2 mb-2 relative">
208210
<button
209-
onClick={onClear}
211+
onClick={() => model.clearError()}
210212
className="absolute top-2 right-2 text-red-400 hover:text-red-300 cursor-pointer z-10"
211213
aria-label="Close error"
212214
>
213215
<i className="fa fa-times text-sm"></i>
214216
</button>
215-
<div className="text-sm pr-6 max-h-[100px] overflow-y-auto">{errorMessage}</div>
217+
<div className="text-sm pr-6 max-h-[100px] overflow-y-auto">
218+
{errorMessage}
219+
<button
220+
onClick={() => model.clearChat()}
221+
className="ml-2 text-xs text-red-300 hover:text-red-200 cursor-pointer underline"
222+
>
223+
New Chat
224+
</button>
225+
</div>
216226
</div>
217227
);
218228
});
219229

220230
AIErrorMessage.displayName = "AIErrorMessage";
221231

222-
const RTInfoModeFixer = memo(() => {
232+
const ConfigChangeModeFixer = memo(() => {
223233
const model = WaveAIModel.getInstance();
224234
const telemetryEnabled = jotai.useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false;
225235
const aiModeConfigs = jotai.useAtomValue(model.aiModeConfigs);
226236

227237
useEffect(() => {
228-
model.fixRTInfoMode();
238+
model.fixModeAfterConfigChange();
229239
}, [telemetryEnabled, aiModeConfigs, model]);
230240

231241
return null;
232242
});
233243

234-
RTInfoModeFixer.displayName = "RTInfoModeFixer";
244+
ConfigChangeModeFixer.displayName = "ConfigChangeModeFixer";
235245

236246
const AIPanelComponentInner = memo(() => {
237247
const [isDragOver, setIsDragOver] = useState(false);
238248
const [isReactDndDragOver, setIsReactDndDragOver] = useState(false);
239249
const [initialLoadDone, setInitialLoadDone] = useState(false);
240250
const model = WaveAIModel.getInstance();
241251
const containerRef = useRef<HTMLDivElement>(null);
242-
const errorMessage = jotai.useAtomValue(model.errorMessage);
243252
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
244253
const showOverlayBlockNums = jotai.useAtomValue(getSettingsKeyAtom("app:showoverlayblocknums")) ?? true;
245254
const isFocused = jotai.useAtomValue(model.isWaveAIFocusedAtom);
@@ -544,7 +553,7 @@ const AIPanelComponentInner = memo(() => {
544553
onClick={handleClick}
545554
inert={!isPanelVisible ? true : undefined}
546555
>
547-
<RTInfoModeFixer />
556+
<ConfigChangeModeFixer />
548557
{(isDragOver || isReactDndDragOver) && allowAccess && <AIDragOverlay />}
549558
{showBlockMask && <AIBlockMask />}
550559
<AIPanelHeader />
@@ -572,9 +581,7 @@ const AIPanelComponentInner = memo(() => {
572581
onContextMenu={(e) => handleWaveAIContextMenu(e, true)}
573582
/>
574583
)}
575-
{errorMessage && (
576-
<AIErrorMessage errorMessage={errorMessage} onClear={() => model.clearError()} />
577-
)}
584+
<AIErrorMessage />
578585
<AIDroppedFiles model={model} />
579586
<AIPanelInput onSubmit={handleSubmit} status={status} model={model} />
580587
</>

frontend/app/aipanel/waveai-model.tsx

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -116,31 +116,27 @@ export class WaveAIModel {
116116

117117
this.defaultModeAtom = jotai.atom((get) => {
118118
const telemetryEnabled = get(getSettingsKeyAtom("telemetry:enabled")) ?? false;
119-
120119
if (this.inBuilder) {
121-
return telemetryEnabled ? "waveai@balanced" : "unknown";
120+
return telemetryEnabled ? "waveai@balanced" : "invalid";
122121
}
123-
124122
const aiModeConfigs = get(this.aiModeConfigs);
123+
if (!telemetryEnabled) {
124+
let mode = get(getSettingsKeyAtom("waveai:defaultmode"));
125+
if (mode == null || mode.startsWith("waveai@")) {
126+
return "unknown";
127+
}
128+
return mode;
129+
}
125130
const hasPremium = get(this.hasPremiumAtom);
126-
127131
const waveFallback = hasPremium ? "waveai@balanced" : "waveai@quick";
128132
let mode = get(getSettingsKeyAtom("waveai:defaultmode")) ?? waveFallback;
129-
133+
if (!hasPremium && mode.startsWith("waveai@")) {
134+
mode = "waveai@quick";
135+
}
130136
const modeExists = aiModeConfigs != null && mode in aiModeConfigs;
131-
132137
if (!modeExists) {
133-
if (telemetryEnabled) {
134-
mode = waveFallback;
135-
} else {
136-
return "unknown";
137-
}
138+
mode = waveFallback;
138139
}
139-
140-
if (mode.startsWith("waveai@") && !telemetryEnabled) {
141-
return "unknown";
142-
}
143-
144140
return mode;
145141
});
146142

@@ -165,6 +161,7 @@ export class WaveAIModel {
165161
orefContext = WOS.makeORef("tab", tabId);
166162
}
167163
WaveAIModel.instance = new WaveAIModel(orefContext, inBuilder);
164+
(window as any).WaveAIModel = WaveAIModel.instance;
168165
}
169166
return WaveAIModel.instance;
170167
}
@@ -273,6 +270,7 @@ export class WaveAIModel {
273270
clearChat() {
274271
this.useChatStop?.();
275272
this.clearFiles();
273+
this.clearError();
276274
this.isChatEmpty = true;
277275
const newChatId = crypto.randomUUID();
278276
globalStore.set(this.chatId, newChatId);
@@ -408,19 +406,23 @@ export class WaveAIModel {
408406
});
409407
}
410408

411-
async fixRTInfoMode(): Promise<void> {
409+
async fixModeAfterConfigChange(): Promise<void> {
412410
const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, {
413411
oref: this.orefContext,
414412
});
415413
const mode = rtInfo?.["waveai:mode"];
416-
if (mode == null) {
417-
return;
418-
}
419-
if (!this.isValidMode(mode)) {
414+
if (mode == null || !this.isValidMode(mode)) {
420415
this.setAIModeToDefault();
421416
}
422417
}
423418

419+
async getRTInfo(): Promise<Record<string, any>> {
420+
const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, {
421+
oref: this.orefContext,
422+
});
423+
return rtInfo ?? {};
424+
}
425+
424426
async loadInitialChat(): Promise<WaveUIMessage[]> {
425427
const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, {
426428
oref: this.orefContext,

0 commit comments

Comments
 (0)