-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Add bottom menu extension selection #5352
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
07abfa3
add bottom menu extension selection for ALPHA
zanesq 4460bf3
Merge branches 'main' and 'zane/next-camp/chat-extension-toggle' of g…
zanesq 37dd326
- Remove unused ExtensionItem component
zanesq 592dce6
- Remove fetchExtensions callback wrapper and unnecessary fetch calls
zanesq File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| import { useCallback, useMemo, useState } from 'react'; | ||
| import { Puzzle } from 'lucide-react'; | ||
| import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../ui/dropdown-menu'; | ||
| import { Input } from '../ui/input'; | ||
| import { Switch } from '../ui/switch'; | ||
| import { FixedExtensionEntry, useConfig } from '../ConfigContext'; | ||
| import { toggleExtension } from '../settings/extensions/extension-manager'; | ||
| import { toastService } from '../../toasts'; | ||
| import { getFriendlyTitle } from '../settings/extensions/subcomponents/ExtensionList'; | ||
|
|
||
| interface BottomMenuExtensionSelectionProps { | ||
| sessionId: string; | ||
| } | ||
|
|
||
| export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionSelectionProps) => { | ||
| const [searchQuery, setSearchQuery] = useState(''); | ||
| const [isOpen, setIsOpen] = useState(false); | ||
| const { extensionsList, addExtension } = useConfig(); | ||
|
|
||
| const handleToggle = useCallback( | ||
| async (extensionConfig: FixedExtensionEntry) => { | ||
| if (!sessionId) { | ||
| toastService.error({ | ||
| title: 'Extension Toggle Error', | ||
| msg: 'No active session found. Please start a chat session first.', | ||
| traceback: 'No session ID available', | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const toggleDirection = extensionConfig.enabled ? 'toggleOff' : 'toggleOn'; | ||
|
|
||
| await toggleExtension({ | ||
| toggle: toggleDirection, | ||
| extensionConfig: extensionConfig, | ||
| addToConfig: addExtension, | ||
| toastOptions: { silent: false }, | ||
| sessionId: sessionId, | ||
| }); | ||
| } catch (error) { | ||
| toastService.error({ | ||
| title: 'Extension Error', | ||
| msg: `Failed to ${extensionConfig.enabled ? 'disable' : 'enable'} ${extensionConfig.name}`, | ||
| traceback: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| } | ||
| }, | ||
| [sessionId, addExtension] | ||
| ); | ||
|
|
||
| const filteredExtensions = useMemo(() => { | ||
| return extensionsList.filter((ext) => { | ||
| const query = searchQuery.toLowerCase(); | ||
| return ( | ||
| ext.name.toLowerCase().includes(query) || | ||
| (ext.description && ext.description.toLowerCase().includes(query)) | ||
| ); | ||
| }); | ||
| }, [extensionsList, searchQuery]); | ||
|
|
||
| const sortedExtensions = useMemo(() => { | ||
| const getTypePriority = (type: string): number => { | ||
| const priorities: Record<string, number> = { | ||
| builtin: 0, | ||
| platform: 1, | ||
| frontend: 2, | ||
| }; | ||
| return priorities[type] ?? Number.MAX_SAFE_INTEGER; | ||
| }; | ||
|
|
||
| return [...filteredExtensions].sort((a, b) => { | ||
| // First sort by priority type | ||
| const typeDiff = getTypePriority(a.type) - getTypePriority(b.type); | ||
| if (typeDiff !== 0) return typeDiff; | ||
|
|
||
| // Then sort by enabled status (enabled first) | ||
| if (a.enabled !== b.enabled) return a.enabled ? -1 : 1; | ||
|
|
||
| // Finally sort alphabetically | ||
| return a.name.localeCompare(b.name); | ||
| }); | ||
| }, [filteredExtensions]); | ||
|
|
||
| const activeCount = useMemo(() => { | ||
| return extensionsList.filter((ext) => ext.enabled).length; | ||
| }, [extensionsList]); | ||
|
|
||
| return ( | ||
| <DropdownMenu | ||
| open={isOpen} | ||
| onOpenChange={(open) => { | ||
| setIsOpen(open); | ||
| if (!open) { | ||
| setSearchQuery(''); // Reset search when closing | ||
| } | ||
| }} | ||
| > | ||
| <DropdownMenuTrigger asChild> | ||
| <button | ||
| className="flex items-center cursor-pointer [&_svg]:size-4 text-text-default/70 hover:text-text-default hover:scale-100 hover:bg-transparent text-xs" | ||
| title="manage extensions" | ||
| > | ||
| <Puzzle className="mr-1 h-4 w-4" /> | ||
| <span>{activeCount}</span> | ||
| </button> | ||
| </DropdownMenuTrigger> | ||
| <DropdownMenuContent side="top" align="center" className="w-64"> | ||
| <div className="p-2"> | ||
| <Input | ||
| type="text" | ||
| placeholder="search extensions..." | ||
| value={searchQuery} | ||
| onChange={(e) => setSearchQuery(e.target.value)} | ||
| className="h-8 text-sm" | ||
| autoFocus | ||
| /> | ||
| </div> | ||
| <div className="max-h-[400px] overflow-y-auto"> | ||
| {sortedExtensions.length === 0 ? ( | ||
| <div className="px-2 py-4 text-center text-sm text-text-default/70"> | ||
| {searchQuery ? 'no extensions found' : 'no extensions available'} | ||
| </div> | ||
| ) : ( | ||
| sortedExtensions.map((ext) => ( | ||
| <div | ||
| key={ext.name} | ||
| className="flex items-center justify-between px-2 py-2 hover:bg-background-hover cursor-pointer" | ||
| onClick={() => handleToggle(ext)} | ||
| title={ext.description || ext.name} | ||
| > | ||
| <div className="text-sm font-medium text-text-default">{getFriendlyTitle(ext)}</div> | ||
| <div onClick={(e) => e.stopPropagation()}> | ||
| <Switch | ||
| checked={ext.enabled} | ||
| onCheckedChange={() => handleToggle(ext)} | ||
| variant="mono" | ||
| /> | ||
| </div> | ||
| </div> | ||
| )) | ||
| )} | ||
| </div> | ||
| </DropdownMenuContent> | ||
| </DropdownMenu> | ||
| ); | ||
| }; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
we seem to have two sorts going on here. let's combine them into one. also the first sort looks at bundled which I don't think really gives us anything. if we wanted to do this, prioritize anything that is of type {Builtin, Platform, Frontend} I would say