-
-
Notifications
You must be signed in to change notification settings - Fork 805
New AI Thinking Control #2543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New AI Thinking Control #2543
Changes from all commits
f7fcd16
32b065b
13920cb
3f280e1
6f3a972
7322d82
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| // Copyright 2025, Command Line Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { atoms } from "@/app/store/global"; | ||
| import { useAtomValue } from "jotai"; | ||
| import { memo, useRef, useState } from "react"; | ||
| import { WaveAIModel } from "./waveai-model"; | ||
|
|
||
| type ThinkingMode = "quick" | "balanced" | "deep"; | ||
|
|
||
| interface ThinkingModeMetadata { | ||
| icon: string; | ||
| name: string; | ||
| desc: string; | ||
| premium: boolean; | ||
| } | ||
|
|
||
| const ThinkingModeData: Record<ThinkingMode, ThinkingModeMetadata> = { | ||
| quick: { | ||
| icon: "fa-bolt", | ||
| name: "Quick", | ||
| desc: "Fastest responses (gpt-5-mini)", | ||
| premium: false, | ||
| }, | ||
| balanced: { | ||
| icon: "fa-sparkles", | ||
| name: "Balanced", | ||
| desc: "Good mix of speed and accuracy\n(gpt-5 with minimal thinking)", | ||
| premium: true, | ||
| }, | ||
| deep: { | ||
| icon: "fa-lightbulb", | ||
| name: "Deep", | ||
| desc: "Slower but most capable\n(gpt-5 with full reasoning)", | ||
| premium: true, | ||
| }, | ||
| }; | ||
|
|
||
| export const ThinkingLevelDropdown = memo(() => { | ||
| const model = WaveAIModel.getInstance(); | ||
| const thinkingMode = useAtomValue(model.thinkingMode); | ||
| const rateLimitInfo = useAtomValue(atoms.waveAIRateLimitInfoAtom); | ||
| const [isOpen, setIsOpen] = useState(false); | ||
| const dropdownRef = useRef<HTMLDivElement>(null); | ||
|
|
||
| const hasPremium = !rateLimitInfo || rateLimitInfo.unknown || rateLimitInfo.preq > 0; | ||
|
|
||
| const handleSelect = (mode: ThinkingMode) => { | ||
| const metadata = ThinkingModeData[mode]; | ||
| if (!hasPremium && metadata.premium) { | ||
| return; | ||
| } | ||
| model.setThinkingMode(mode); | ||
| setIsOpen(false); | ||
| }; | ||
|
|
||
| let currentMode = (thinkingMode as ThinkingMode) || "balanced"; | ||
| const currentMetadata = ThinkingModeData[currentMode]; | ||
| if (!hasPremium && currentMetadata.premium) { | ||
| currentMode = "quick"; | ||
| } | ||
|
|
||
|
Comment on lines
+57
to
+62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent UI state when premium mode is restricted. Three issues:
Recommended fix: - let currentMode = (thinkingMode as ThinkingMode) || "balanced";
+ const validModes: ThinkingMode[] = ["quick", "balanced", "deep"];
+ let currentMode = validModes.includes(thinkingMode as ThinkingMode)
+ ? (thinkingMode as ThinkingMode)
+ : "quick";
const currentMetadata = ThinkingModeData[currentMode];
if (!hasPremium && currentMetadata.premium) {
currentMode = "quick";
+ // Persist the downgrade if needed
+ if (thinkingMode !== "quick") {
+ model.setThinkingMode("quick");
+ }
}Note: Re-introducing a setter during render requires guarding against redundant calls (as shown above) to avoid duplicate RPCs in React 19 strict mode. Alternatively, use a |
||
| return ( | ||
| <div className="relative" ref={dropdownRef}> | ||
| <button | ||
| onClick={() => setIsOpen(!isOpen)} | ||
| className="flex items-center gap-1.5 px-2 py-1 text-xs text-gray-300 hover:text-white bg-gray-800/50 hover:bg-gray-700/50 rounded transition-colors cursor-pointer border border-gray-600/50" | ||
| title={`Thinking: ${currentMetadata.name}`} | ||
| > | ||
| <i className={`fa ${currentMetadata.icon} text-[10px]`}></i> | ||
| <span className="text-[11px]">{currentMetadata.name}</span> | ||
| <i className="fa fa-chevron-down text-[8px]"></i> | ||
| </button> | ||
|
|
||
| {isOpen && ( | ||
| <> | ||
| <div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} /> | ||
| <div className="absolute top-full right-0 mt-1 bg-gray-800 border border-gray-600 rounded shadow-lg z-50 min-w-[280px]"> | ||
| {(Object.keys(ThinkingModeData) as ThinkingMode[]).map((mode, index) => { | ||
| const metadata = ThinkingModeData[mode]; | ||
| const isFirst = index === 0; | ||
| const isLast = index === Object.keys(ThinkingModeData).length - 1; | ||
| const isDisabled = !hasPremium && metadata.premium; | ||
| const isSelected = currentMode === mode; | ||
| return ( | ||
| <button | ||
| key={mode} | ||
| onClick={() => handleSelect(mode)} | ||
| disabled={isDisabled} | ||
| className={`w-full flex flex-col gap-0.5 px-3 ${ | ||
| isFirst ? "pt-1 pb-0.5" : isLast ? "pt-0.5 pb-1" : "pt-0.5 pb-0.5" | ||
| } ${ | ||
| isDisabled | ||
| ? "text-gray-500 cursor-not-allowed" | ||
| : "text-gray-300 hover:bg-gray-700 cursor-pointer" | ||
| } transition-colors text-left`} | ||
| > | ||
| <div className="flex items-center gap-2 w-full"> | ||
| <i className={`fa ${metadata.icon}`}></i> | ||
| <span className={`text-sm ${isSelected ? "font-bold" : ""}`}> | ||
| {metadata.name} | ||
| {isDisabled && " (premium)"} | ||
| </span> | ||
| {isSelected && <i className="fa fa-check ml-auto"></i>} | ||
| </div> | ||
| <div className="text-xs text-muted pl-5" style={{ whiteSpace: "pre-line" }}> | ||
| {metadata.desc} | ||
| </div> | ||
| </button> | ||
| ); | ||
| })} | ||
| </div> | ||
| </> | ||
| )} | ||
| </div> | ||
| ); | ||
| }); | ||
|
|
||
| ThinkingLevelDropdown.displayName = "ThinkingLevelDropdown"; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
State divergence and validation issues remain unresolved.
The concerns raised in previous reviews at lines 57-62 persist:
State divergence: When a non-premium user has a premium mode stored in
thinkingMode,currentModeis reassigned to"quick"(line 60), but the atom is never updated viamodel.setThinkingMode("quick"). The dropdown menu correctly usescurrentModefor the selection indicator (line 84), but the underlying atom still holds the premium value, causing inconsistency if other code reads from the atom.Type-safety gap: The cast
(thinkingMode as ThinkingMode)on line 57 assumes the atom contains a valid mode. If the atom holds corrupted or stale data (e.g., a deprecated mode name), line 58 will accessundefinedfromThinkingModeData[currentMode], causing a runtime error at line 59 when accessingcurrentMetadata.premium.Inconsistent fallback: The default
"balanced"on line 57 contradicts the PR objective to default non-premium users to"quick". Since"balanced"is premium, the immediate reassignment to"quick"at line 60 masks this issue but adds unnecessary logic.Recommended fix:
Note: Using
queueMicrotask(oruseEffectwith[hasPremium, thinkingMode]dependencies) ensures the state update happens after render, avoiding side effects during render which React 19 strict mode double-invokes.🤖 Prompt for AI Agents