| 1 | import { ProviderInteractionMode, RuntimeMode } from "@t3tools/contracts"; |
| 2 | import { memo, type ReactNode } from "react"; |
| 3 | import { EllipsisIcon } from "lucide-react"; |
| 4 | import { |
| 5 | Menu, |
| 6 | MenuPopup, |
| 7 | MenuRadioGroup, |
| 8 | MenuRadioItem, |
| 9 | MenuSeparator as MenuDivider, |
| 10 | MenuTrigger, |
| 11 | } from "../ui/menu"; |
| 12 | import { ComposerControl, ComposerControlIcon } from "./ComposerControl"; |
| 13 | import { useComposerMenuProps } from "./composerEventScope"; |
| 14 | import { useComposerMenuState } from "./useComposerMenuState"; |
| 15 | |
| 16 | export const CompactComposerControlsMenu = memo(function CompactComposerControlsMenu(props: { |
| 17 | interactionMode: ProviderInteractionMode; |
| 18 | runtimeMode: RuntimeMode; |
| 19 | runtimeModeOptions: ReadonlyArray<{ |
| 20 | readonly mode: RuntimeMode; |
| 21 | readonly label: string; |
| 22 | }>; |
| 23 | showInteractionModeToggle: boolean; |
| 24 | traitsMenuContent?: ReactNode; |
| 25 | size?: "sm" | "xs"; |
| 26 | /** |
| 27 | * The resting strip keeps this menu mounted out of flow while every block |
| 28 | * fits inline. Its portaled popup would outlive that transition, so an |
| 29 | * open menu closes when its trigger hides. |
| 30 | */ |
| 31 | hidden?: boolean; |
| 32 | onToggleInteractionMode: () => void; |
| 33 | onRuntimeModeChange: (mode: RuntimeMode) => void; |
| 34 | }) { |
| 35 | const composerFloatingLayerProps = useComposerMenuProps(); |
| 36 | const size = props.size ?? "sm"; |
| 37 | const [open, setOpen] = useComposerMenuState(props.hidden); |
| 38 | |
| 39 | return ( |
| 40 | <Menu open={open} onOpenChange={setOpen}> |
| 41 | <MenuTrigger |
| 42 | render={ |
| 43 | <ComposerControl |
| 44 | size={size} |
| 45 | className="shrink-0" |
| 46 | aria-label="More composer controls" |
| 47 | data-composer-shortcut={ |
| 48 | props.traitsMenuContent ? "composer.mode composer.effort" : "composer.mode" |
| 49 | } |
| 50 | /> |
| 51 | } |
| 52 | > |
| 53 | <ComposerControlIcon icon={EllipsisIcon} size={size} /> |
| 54 | </MenuTrigger> |
| 55 | <MenuPopup align="start" {...composerFloatingLayerProps}> |
| 56 | {props.traitsMenuContent ? ( |
| 57 | <> |
| 58 | {props.traitsMenuContent} |
| 59 | <MenuDivider /> |
| 60 | </> |
| 61 | ) : null} |
| 62 | {props.showInteractionModeToggle ? ( |
| 63 | <> |
| 64 | <div className="px-2 py-1.5 font-medium text-muted-foreground text-xs">Mode</div> |
| 65 | <MenuRadioGroup |
| 66 | value={props.interactionMode} |
| 67 | onValueChange={(value) => { |
| 68 | if (!value || value === props.interactionMode) return; |
| 69 | props.onToggleInteractionMode(); |
| 70 | }} |
| 71 | > |
| 72 | <MenuRadioItem value="default">Chat</MenuRadioItem> |
| 73 | <MenuRadioItem value="plan">Plan</MenuRadioItem> |
| 74 | </MenuRadioGroup> |
| 75 | <MenuDivider /> |
| 76 | </> |
| 77 | ) : null} |
| 78 | <div className="px-2 py-1.5 font-medium text-muted-foreground text-xs">Access</div> |
| 79 | <MenuRadioGroup |
| 80 | value={props.runtimeMode} |
| 81 | onValueChange={(value) => { |
| 82 | if (!value || value === props.runtimeMode) return; |
| 83 | props.onRuntimeModeChange(value as RuntimeMode); |
| 84 | }} |
| 85 | > |
| 86 | {props.runtimeModeOptions.map((option) => ( |
| 87 | <MenuRadioItem key={option.mode} value={option.mode}> |
| 88 | {option.label} |
| 89 | </MenuRadioItem> |
| 90 | ))} |
| 91 | </MenuRadioGroup> |
| 92 | </MenuPopup> |
| 93 | </Menu> |
| 94 | ); |
| 95 | }); |