diff --git a/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ChatComposerControls.tsx b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ChatComposerControls.tsx index 7039c5cb8a9..3e7b016d833 100644 --- a/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ChatComposerControls.tsx +++ b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ChatComposerControls.tsx @@ -3,18 +3,14 @@ import { PromptInputSubmit, PromptInputTools, } from "@superset/ui/ai-elements/prompt-input"; -import { - type ThinkingLevel, - ThinkingToggle, -} from "@superset/ui/ai-elements/thinking-toggle"; +import type { ThinkingLevel } from "@superset/ui/ai-elements/thinking-toggle"; import type { ChatStatus } from "ai"; import { ArrowUpIcon, Loader2Icon, SquareIcon } from "lucide-react"; import type React from "react"; -import { PILL_BUTTON_CLASS } from "../../../../styles"; import type { ModelOption, PermissionMode } from "../../../../types"; import { ModelPicker } from "../../../ModelPicker"; -import { PermissionModePicker } from "../../../PermissionModePicker"; import { PlusMenu } from "../../../PlusMenu"; +import { ComposerSettingsMenu } from "./ComposerSettingsMenu"; interface ChatComposerControlsProps { availableModels: ModelOption[]; @@ -50,22 +46,22 @@ export function ChatComposerControls({ return ( - -
diff --git a/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx new file mode 100644 index 00000000000..6e807c223d1 --- /dev/null +++ b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx @@ -0,0 +1,234 @@ +import { ModelSelectorLogo } from "@superset/ui/ai-elements/model-selector"; +import { PromptInputButton } from "@superset/ui/ai-elements/prompt-input"; +import type { ThinkingLevel } from "@superset/ui/ai-elements/thinking-toggle"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@superset/ui/dropdown-menu"; +import { claudeIcon } from "@superset/ui/icons/preset-icons"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { + BrainIcon, + CheckIcon, + ChevronRightIcon, + ShieldCheckIcon, + ShieldIcon, + ShieldOffIcon, +} from "lucide-react"; +import type React from "react"; +import { useRef, useState } from "react"; +import { PILL_BUTTON_CLASS } from "renderer/components/Chat/ChatInterface/styles"; +import type { + ModelOption, + PermissionMode, +} from "renderer/components/Chat/ChatInterface/types"; +import { + ANTHROPIC_LOGO_PROVIDER, + providerToLogo, +} from "../../../../ModelPicker/utils/providerToLogo"; + +interface ComposerSettingsMenuProps { + selectedModel: ModelOption | null; + setModelSelectorOpen: React.Dispatch>; + permissionMode: PermissionMode; + setPermissionMode: React.Dispatch>; + thinkingLevel: ThinkingLevel; + setThinkingLevel: (level: ThinkingLevel) => void; +} + +interface PermissionModeOption { + value: PermissionMode; + label: string; + description: string; + icon: React.ComponentType<{ className?: string }>; +} + +const PERMISSION_MODES: PermissionModeOption[] = [ + { + value: "bypassPermissions", + label: "Auto", + description: "Tools run without approval", + icon: ShieldOffIcon, + }, + { + value: "acceptEdits", + label: "Semi-auto", + description: "Edits auto-approved, others need approval", + icon: ShieldCheckIcon, + }, + { + value: "default", + label: "Manual", + description: "All tools require approval", + icon: ShieldIcon, + }, +]; + +interface ThinkingLevelOption { + value: ThinkingLevel; + label: string; + description: string; +} + +const THINKING_LEVELS: ThinkingLevelOption[] = [ + { value: "off", label: "Off", description: "No extended thinking" }, + { value: "low", label: "Low", description: "Minimal reasoning effort" }, + { + value: "medium", + label: "Medium", + description: "Moderate reasoning effort", + }, + { value: "high", label: "High", description: "Thorough reasoning effort" }, + { + value: "xhigh", + label: "Max", + description: "Maximum reasoning effort", + }, +]; + +export function ComposerSettingsMenu({ + selectedModel, + setModelSelectorOpen, + permissionMode, + setPermissionMode, + thinkingLevel, + setThinkingLevel, +}: ComposerSettingsMenuProps) { + const [menuOpen, setMenuOpen] = useState(false); + const pendingDialogOpenRef = useRef(false); + + const activePermission = + PERMISSION_MODES.find((m) => m.value === permissionMode) ?? + PERMISSION_MODES[0]; + const PermissionIcon = activePermission.icon; + + const activeThinking = + THINKING_LEVELS.find((t) => t.value === thinkingLevel) ?? + THINKING_LEVELS[0]; + + const brainIconColor = + thinkingLevel === "off" ? "text-muted-foreground" : "text-foreground"; + + const selectedLogo = selectedModel + ? providerToLogo(selectedModel.provider) + : null; + + const tooltipText = `Model: ${selectedModel?.name ?? "Model"} · Permission: ${activePermission.label} · Thinking: ${activeThinking.label}`; + + const ariaLabel = `Chat settings: model ${selectedModel?.name ?? "Model"}, permission ${activePermission.label}, thinking ${activeThinking.label}`; + + return ( + + + + + + + {selectedLogo === ANTHROPIC_LOGO_PROVIDER ? ( + Claude + ) : selectedLogo ? ( + + ) : null} + + {selectedModel?.name ?? "Model"} + + + + + + +

{tooltipText}

+
+
+ { + if (pendingDialogOpenRef.current) { + event.preventDefault(); + pendingDialogOpenRef.current = false; + setModelSelectorOpen(true); + } + }} + > + Permission + {PERMISSION_MODES.map((mode) => { + const Icon = mode.icon; + const isActive = mode.value === permissionMode; + return ( + setPermissionMode(mode.value)} + className="flex items-center gap-2" + > + +
+ {mode.label} + + {mode.description} + +
+ {isActive && } +
+ ); + })} + + + + Thinking + {THINKING_LEVELS.map((level) => { + const isActive = level.value === thinkingLevel; + return ( + setThinkingLevel(level.value)} + className="flex items-center gap-2" + > +
+ {level.label} + + {level.description} + +
+ {isActive && } +
+ ); + })} + + + +
+ +
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/index.ts b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/index.ts new file mode 100644 index 00000000000..bd5ca97c151 --- /dev/null +++ b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/index.ts @@ -0,0 +1 @@ +export { ComposerSettingsMenu } from "./ComposerSettingsMenu"; diff --git a/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ModelPicker/ModelPicker.tsx b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ModelPicker/ModelPicker.tsx index 123a73ead30..cf3e46a6449 100644 --- a/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ModelPicker/ModelPicker.tsx +++ b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/ModelPicker/ModelPicker.tsx @@ -28,6 +28,7 @@ interface ModelPickerProps { onSelectModel: (model: ModelOption) => void; open: boolean; onOpenChange: (open: boolean) => void; + triggerless?: boolean; } export function ModelPicker({ @@ -36,6 +37,7 @@ export function ModelPicker({ onSelectModel, open, onOpenChange, + triggerless = false, }: ModelPickerProps) { const navigate = useNavigate(); const groupedModels = useMemo(() => groupModelsByProvider(models), [models]); @@ -59,19 +61,21 @@ export function ModelPicker({ return ( - - - {selectedLogo === ANTHROPIC_LOGO_PROVIDER ? ( - Claude - ) : selectedLogo ? ( - - ) : null} - {selectedModel?.name ?? "Model"} - - - + {!triggerless && ( + + + {selectedLogo === ANTHROPIC_LOGO_PROVIDER ? ( + Claude + ) : selectedLogo ? ( + + ) : null} + {selectedModel?.name ?? "Model"} + + + + )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ChatComposerControls.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ChatComposerControls.tsx index 80412316c4b..62a501cc2ce 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ChatComposerControls.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ChatComposerControls.tsx @@ -3,21 +3,17 @@ import { PromptInputSubmit, PromptInputTools, } from "@superset/ui/ai-elements/prompt-input"; -import { - type ThinkingLevel, - ThinkingToggle, -} from "@superset/ui/ai-elements/thinking-toggle"; +import type { ThinkingLevel } from "@superset/ui/ai-elements/thinking-toggle"; import type { ChatStatus } from "ai"; import { ArrowUpIcon, Loader2Icon, SquareIcon } from "lucide-react"; import type React from "react"; -import { PermissionModePicker } from "renderer/components/Chat/ChatInterface/components/PermissionModePicker"; import { PlusMenu } from "renderer/components/Chat/ChatInterface/components/PlusMenu"; -import { PILL_BUTTON_CLASS } from "renderer/components/Chat/ChatInterface/styles"; import type { ModelOption, PermissionMode, } from "renderer/components/Chat/ChatInterface/types"; import { ModelPicker } from "../../../ModelPicker"; +import { ComposerSettingsMenu } from "./ComposerSettingsMenu"; interface ChatComposerControlsProps { availableModels: ModelOption[]; @@ -53,22 +49,22 @@ export function ChatComposerControls({ return ( - -
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx new file mode 100644 index 00000000000..6e807c223d1 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/ComposerSettingsMenu.tsx @@ -0,0 +1,234 @@ +import { ModelSelectorLogo } from "@superset/ui/ai-elements/model-selector"; +import { PromptInputButton } from "@superset/ui/ai-elements/prompt-input"; +import type { ThinkingLevel } from "@superset/ui/ai-elements/thinking-toggle"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@superset/ui/dropdown-menu"; +import { claudeIcon } from "@superset/ui/icons/preset-icons"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { + BrainIcon, + CheckIcon, + ChevronRightIcon, + ShieldCheckIcon, + ShieldIcon, + ShieldOffIcon, +} from "lucide-react"; +import type React from "react"; +import { useRef, useState } from "react"; +import { PILL_BUTTON_CLASS } from "renderer/components/Chat/ChatInterface/styles"; +import type { + ModelOption, + PermissionMode, +} from "renderer/components/Chat/ChatInterface/types"; +import { + ANTHROPIC_LOGO_PROVIDER, + providerToLogo, +} from "../../../../ModelPicker/utils/providerToLogo"; + +interface ComposerSettingsMenuProps { + selectedModel: ModelOption | null; + setModelSelectorOpen: React.Dispatch>; + permissionMode: PermissionMode; + setPermissionMode: React.Dispatch>; + thinkingLevel: ThinkingLevel; + setThinkingLevel: (level: ThinkingLevel) => void; +} + +interface PermissionModeOption { + value: PermissionMode; + label: string; + description: string; + icon: React.ComponentType<{ className?: string }>; +} + +const PERMISSION_MODES: PermissionModeOption[] = [ + { + value: "bypassPermissions", + label: "Auto", + description: "Tools run without approval", + icon: ShieldOffIcon, + }, + { + value: "acceptEdits", + label: "Semi-auto", + description: "Edits auto-approved, others need approval", + icon: ShieldCheckIcon, + }, + { + value: "default", + label: "Manual", + description: "All tools require approval", + icon: ShieldIcon, + }, +]; + +interface ThinkingLevelOption { + value: ThinkingLevel; + label: string; + description: string; +} + +const THINKING_LEVELS: ThinkingLevelOption[] = [ + { value: "off", label: "Off", description: "No extended thinking" }, + { value: "low", label: "Low", description: "Minimal reasoning effort" }, + { + value: "medium", + label: "Medium", + description: "Moderate reasoning effort", + }, + { value: "high", label: "High", description: "Thorough reasoning effort" }, + { + value: "xhigh", + label: "Max", + description: "Maximum reasoning effort", + }, +]; + +export function ComposerSettingsMenu({ + selectedModel, + setModelSelectorOpen, + permissionMode, + setPermissionMode, + thinkingLevel, + setThinkingLevel, +}: ComposerSettingsMenuProps) { + const [menuOpen, setMenuOpen] = useState(false); + const pendingDialogOpenRef = useRef(false); + + const activePermission = + PERMISSION_MODES.find((m) => m.value === permissionMode) ?? + PERMISSION_MODES[0]; + const PermissionIcon = activePermission.icon; + + const activeThinking = + THINKING_LEVELS.find((t) => t.value === thinkingLevel) ?? + THINKING_LEVELS[0]; + + const brainIconColor = + thinkingLevel === "off" ? "text-muted-foreground" : "text-foreground"; + + const selectedLogo = selectedModel + ? providerToLogo(selectedModel.provider) + : null; + + const tooltipText = `Model: ${selectedModel?.name ?? "Model"} · Permission: ${activePermission.label} · Thinking: ${activeThinking.label}`; + + const ariaLabel = `Chat settings: model ${selectedModel?.name ?? "Model"}, permission ${activePermission.label}, thinking ${activeThinking.label}`; + + return ( + + + + + + + {selectedLogo === ANTHROPIC_LOGO_PROVIDER ? ( + Claude + ) : selectedLogo ? ( + + ) : null} + + {selectedModel?.name ?? "Model"} + + + + + + +

{tooltipText}

+
+
+ { + if (pendingDialogOpenRef.current) { + event.preventDefault(); + pendingDialogOpenRef.current = false; + setModelSelectorOpen(true); + } + }} + > + Permission + {PERMISSION_MODES.map((mode) => { + const Icon = mode.icon; + const isActive = mode.value === permissionMode; + return ( + setPermissionMode(mode.value)} + className="flex items-center gap-2" + > + +
+ {mode.label} + + {mode.description} + +
+ {isActive && } +
+ ); + })} + + + + Thinking + {THINKING_LEVELS.map((level) => { + const isActive = level.value === thinkingLevel; + return ( + setThinkingLevel(level.value)} + className="flex items-center gap-2" + > +
+ {level.label} + + {level.description} + +
+ {isActive && } +
+ ); + })} + + + +
+ +
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/index.ts new file mode 100644 index 00000000000..bd5ca97c151 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatComposerControls/ComposerSettingsMenu/index.ts @@ -0,0 +1 @@ +export { ComposerSettingsMenu } from "./ComposerSettingsMenu"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ModelPicker/ModelPicker.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ModelPicker/ModelPicker.tsx index d9b03ee2a41..7b4cac9191a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ModelPicker/ModelPicker.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ModelPicker/ModelPicker.tsx @@ -28,6 +28,7 @@ interface ModelPickerProps { onSelectModel: (model: ModelOption) => void; open: boolean; onOpenChange: (open: boolean) => void; + triggerless?: boolean; } export function ModelPicker({ @@ -36,6 +37,7 @@ export function ModelPicker({ onSelectModel, open, onOpenChange, + triggerless = false, }: ModelPickerProps) { const navigate = useNavigate(); const groupedModels = useMemo(() => groupModelsByProvider(models), [models]); @@ -59,19 +61,21 @@ export function ModelPicker({ return ( - - - {selectedLogo === ANTHROPIC_LOGO_PROVIDER ? ( - Claude - ) : selectedLogo ? ( - - ) : null} - {selectedModel?.name ?? "Model"} - - - + {!triggerless && ( + + + {selectedLogo === ANTHROPIC_LOGO_PROVIDER ? ( + Claude + ) : selectedLogo ? ( + + ) : null} + {selectedModel?.name ?? "Model"} + + + + )}