diff --git a/gui/src/components/ConversationStarters/ConversationStarterCard.tsx b/gui/src/components/ConversationStarters/ConversationStarterCard.tsx index f0428a7232..29ff818330 100644 --- a/gui/src/components/ConversationStarters/ConversationStarterCard.tsx +++ b/gui/src/components/ConversationStarters/ConversationStarterCard.tsx @@ -1,7 +1,5 @@ import { ChatBubbleLeftIcon } from "@heroicons/react/24/outline"; import { SlashCommandDescription } from "core"; -import { useState } from "react"; -import { defaultBorderRadius, vscInputBackground } from ".."; interface ConversationStarterCardProps { command: SlashCommandDescription; @@ -12,26 +10,20 @@ export function ConversationStarterCard({ command, onClick, }: ConversationStarterCardProps) { - const [isHovered, setIsHovered] = useState(false); - return (
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} + className="bg-vsc-input-background mb-2 w-full rounded-md shadow-md hover:cursor-pointer hover:brightness-110" onClick={() => onClick(command)} >
-
+
{command.name}
-
{command.description}
+ {command.description && ( +
{command.description}
+ )}
diff --git a/gui/src/components/ConversationStarters/ConversationStarterCards.tsx b/gui/src/components/ConversationStarters/ConversationStarterCards.tsx index 2a29d07498..7e0e2e1835 100644 --- a/gui/src/components/ConversationStarters/ConversationStarterCards.tsx +++ b/gui/src/components/ConversationStarters/ConversationStarterCards.tsx @@ -1,15 +1,24 @@ import { SlashCommandDescription } from "core"; -import { useAppDispatch } from "../../redux/hooks"; +import { useState } from "react"; +import { useBookmarkedSlashCommands } from "../../hooks/useBookmarkedSlashCommands"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks"; import { setMainEditorContentTrigger } from "../../redux/slices/sessionSlice"; import { getParagraphNodeFromString } from "../mainInput/utils"; import { ConversationStarterCard } from "./ConversationStarterCard"; -import { useBookmarkedSlashCommands } from "./useBookmarkedSlashCommands"; -const NUM_CARDS_TO_RENDER = 3; +const NUM_CARDS_TO_RENDER = 5; export function ConversationStarterCards() { const dispatch = useAppDispatch(); - const { cmdsSortedByBookmark } = useBookmarkedSlashCommands(); + const [showAll, setShowAll] = useState(false); + const { isCommandBookmarked } = useBookmarkedSlashCommands(); + const slashCommands = useAppSelector( + (state) => state.config.config.slashCommands ?? [], + ); + + const bookmarkedSlashCommands = slashCommands.filter((command) => + isCommandBookmarked(command.name), + ); function onClick(command: SlashCommandDescription) { if (command.prompt) { @@ -19,19 +28,35 @@ export function ConversationStarterCards() { } } - if (!cmdsSortedByBookmark || cmdsSortedByBookmark.length === 0) { + if (bookmarkedSlashCommands.length === 0) { return null; } + const visibleCommands = showAll + ? bookmarkedSlashCommands + : bookmarkedSlashCommands.slice(0, NUM_CARDS_TO_RENDER); + + const remainingCount = bookmarkedSlashCommands.length - NUM_CARDS_TO_RENDER; + return ( -
- {cmdsSortedByBookmark.slice(0, NUM_CARDS_TO_RENDER).map((command, i) => ( - - ))} +
+
+ {visibleCommands.map((command, i) => ( + + ))} +
+ {bookmarkedSlashCommands.length > NUM_CARDS_TO_RENDER && ( + setShowAll(!showAll)} + > + {showAll ? "Show less" : `Show ${remainingCount} more...`} + + )}
); } diff --git a/gui/src/components/ConversationStarters/useBookmarkedSlashCommands.ts b/gui/src/components/ConversationStarters/useBookmarkedSlashCommands.ts deleted file mode 100644 index 786ae379a0..0000000000 --- a/gui/src/components/ConversationStarters/useBookmarkedSlashCommands.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { SlashCommandDescription } from "core"; -import { usePostHog } from "posthog-js/react"; -import { useMemo } from "react"; -import { - bookmarkSlashCommand, - selectBookmarkedSlashCommands, - selectSelectedProfileId, - unbookmarkSlashCommand, -} from "../../redux"; -import { useAppDispatch, useAppSelector } from "../../redux/hooks"; -import { isDeprecatedCommandName, sortCommandsByBookmarkStatus } from "./utils"; - -export function useBookmarkedSlashCommands() { - const dispatch = useAppDispatch(); - const posthog = usePostHog(); - - const slashCommands = - useAppSelector((state) => state.config.config.slashCommands) ?? []; - const selectedProfileId = useAppSelector(selectSelectedProfileId); - const bookmarkedCommands = useAppSelector(selectBookmarkedSlashCommands); - - const filteredSlashCommands = slashCommands.filter(isDeprecatedCommandName); - - // Create a map of command names to bookmark status - const bookmarkStatuses = useMemo(() => { - const statuses: Record = {}; - if (selectedProfileId) { - filteredSlashCommands.forEach((command) => { - statuses[command.name] = bookmarkedCommands.includes(command.name); - }); - } - return statuses; - }, [filteredSlashCommands, bookmarkedCommands, selectedProfileId]); - - // Sort commands by bookmark status - const cmdsSortedByBookmark = useMemo( - () => - sortCommandsByBookmarkStatus(filteredSlashCommands, bookmarkedCommands), - [filteredSlashCommands, bookmarkedCommands], - ); - - const toggleBookmark = (command: SlashCommandDescription) => { - const isBookmarked = bookmarkStatuses[command.name]; - - posthog.capture("toggle_bookmarked_slash_command", { - isBookmarked, - }); - - if (isBookmarked) { - dispatch( - unbookmarkSlashCommand({ - commandName: command.name, - }), - ); - } else { - dispatch( - bookmarkSlashCommand({ - commandName: command.name, - }), - ); - } - }; - - return { - cmdsSortedByBookmark, - bookmarkStatuses, - toggleBookmark, - }; -} diff --git a/gui/src/components/mainInput/Lump/BlockSettingsTopToolbar.tsx b/gui/src/components/mainInput/Lump/BlockSettingsTopToolbar.tsx index a561d2e1c1..433770b3d3 100644 --- a/gui/src/components/mainInput/Lump/BlockSettingsTopToolbar.tsx +++ b/gui/src/components/mainInput/Lump/BlockSettingsTopToolbar.tsx @@ -21,6 +21,7 @@ interface BlockSettingsToolbarIcon { itemCount?: number; onClick: () => void; isSelected?: boolean; + className?: string; } interface Section { @@ -53,7 +54,7 @@ function BlockSettingsToolbarIcon(props: BlockSettingsToolbarIcon) { style={{ backgroundColor: props.isSelected ? vscBadgeBackground : undefined, }} - className={`relative flex select-none items-center rounded-full px-1 transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50`} + className={`relative flex select-none items-center rounded-full px-1 transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 ${props.className || ""}`} > -
-
- -
-
- {sections.map((section) => ( - - props.setSelectedSection( - props.selectedSection === section.id ? null : section.id, - ) - } - /> - ))} -
+
+
+ +
+
+ {sections.map((section) => ( + + props.setSelectedSection( + props.selectedSection === section.id ? null : section.id, + ) + } + /> + ))}
+
+ +
); diff --git a/gui/src/components/mainInput/Lump/sections/PromptsSection.tsx b/gui/src/components/mainInput/Lump/sections/PromptsSection.tsx index 21a3e294bc..6ad5368091 100644 --- a/gui/src/components/mainInput/Lump/sections/PromptsSection.tsx +++ b/gui/src/components/mainInput/Lump/sections/PromptsSection.tsx @@ -3,8 +3,9 @@ import { PencilIcon, } from "@heroicons/react/24/outline"; import { BookmarkIcon as BookmarkSolid } from "@heroicons/react/24/solid"; +import { useBookmarkedSlashCommands } from "../../../../hooks/useBookmarkedSlashCommands"; +import { useAppSelector } from "../../../../redux/hooks"; import { fontSize } from "../../../../util"; -import { useBookmarkedSlashCommands } from "../../../ConversationStarters/useBookmarkedSlashCommands"; import { ExploreBlocksButton } from "./ExploreBlocksButton"; interface PromptRowProps { @@ -54,22 +55,32 @@ function PromptRow({ } export function PromptsSection() { - const { cmdsSortedByBookmark, bookmarkStatuses, toggleBookmark } = - useBookmarkedSlashCommands(); + const { isCommandBookmarked, toggleBookmark } = useBookmarkedSlashCommands(); + const slashCommands = useAppSelector( + (state) => state.config.config.slashCommands ?? [], + ); const handleEdit = (prompt: any) => { // Handle edit action here console.log("Editing prompt:", prompt); }; + const sortedCommands = [...slashCommands].sort((a, b) => { + const aBookmarked = isCommandBookmarked(a.name); + const bBookmarked = isCommandBookmarked(b.name); + if (aBookmarked && !bBookmarked) return -1; + if (!aBookmarked && bBookmarked) return 1; + return 0; + }); + return (
- {cmdsSortedByBookmark?.map((prompt, i) => ( + {sortedCommands.map((prompt) => ( toggleBookmark(prompt)} onEdit={() => handleEdit(prompt)} /> diff --git a/gui/src/hooks/useBookmarkedSlashCommands.ts b/gui/src/hooks/useBookmarkedSlashCommands.ts new file mode 100644 index 0000000000..d00c609346 --- /dev/null +++ b/gui/src/hooks/useBookmarkedSlashCommands.ts @@ -0,0 +1,45 @@ +import { SlashCommandDescription } from "core"; +import { usePostHog } from "posthog-js/react"; +import { + bookmarkSlashCommand, + selectBookmarkedSlashCommands, + unbookmarkSlashCommand, +} from "../redux"; +import { useAppDispatch, useAppSelector } from "../redux/hooks"; + +export function useBookmarkedSlashCommands() { + const dispatch = useAppDispatch(); + const posthog = usePostHog(); + const bookmarkedCommands = useAppSelector(selectBookmarkedSlashCommands); + + const isCommandBookmarked = (commandName: string): boolean => { + return bookmarkedCommands.includes(commandName); + }; + + const toggleBookmark = (command: SlashCommandDescription) => { + const isBookmarked = isCommandBookmarked(command.name); + + posthog.capture("toggle_bookmarked_slash_command", { + isBookmarked, + }); + + if (isBookmarked) { + dispatch( + unbookmarkSlashCommand({ + commandName: command.name, + }), + ); + } else { + dispatch( + bookmarkSlashCommand({ + commandName: command.name, + }), + ); + } + }; + + return { + isCommandBookmarked, + toggleBookmark, + }; +} diff --git a/gui/src/hooks/useSetup.ts b/gui/src/hooks/useSetup.ts index 974787f503..d90259a9b4 100644 --- a/gui/src/hooks/useSetup.ts +++ b/gui/src/hooks/useSetup.ts @@ -4,7 +4,11 @@ import { IdeMessengerContext } from "../context/IdeMessenger"; import { ConfigResult } from "@continuedev/config-yaml"; import { BrowserSerializedContinueConfig } from "core"; -import { selectProfileThunk } from "../redux"; +import { + initializeProfilePreferencesThunk, + selectProfileThunk, + selectSelectedProfileId, +} from "../redux"; import { useAppDispatch, useAppSelector } from "../redux/hooks"; import { selectDefaultModel, @@ -28,6 +32,7 @@ function useSetup() { const ideMessenger = useContext(IdeMessengerContext); const history = useAppSelector((store) => store.session.history); const defaultModel = useAppSelector(selectDefaultModel); + const selectedProfileId = useAppSelector(selectSelectedProfileId); const hasLoadedConfig = useRef(false); @@ -47,6 +52,12 @@ function useSetup() { dispatch(setConfigResult(configResult)); dispatch(selectProfileThunk(profileId)); + const isNewProfileId = profileId && profileId !== selectedProfileId; + + if (isNewProfileId) { + dispatch(initializeProfilePreferencesThunk({ profileId })); + } + // Perform any actions needed with the config if (configResult.config?.ui?.fontSize) { setLocalStorage("fontSize", configResult.config.ui.fontSize); diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx index 653efcd3df..50f1291ec5 100644 --- a/gui/src/pages/gui/Chat.tsx +++ b/gui/src/pages/gui/Chat.tsx @@ -427,7 +427,7 @@ export function Chat() { className="flex items-center gap-2" > - Last Session + Last Session
)} diff --git a/gui/src/redux/slices/profiles/slice.ts b/gui/src/redux/slices/profiles/slice.ts index 2d3386a5fc..c8eabf1683 100644 --- a/gui/src/redux/slices/profiles/slice.ts +++ b/gui/src/redux/slices/profiles/slice.ts @@ -1,9 +1,15 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { SlashCommandDescription } from "core"; import { ProfileDescription } from "core/config/ConfigHandler"; -import { ensureProfilePreferences } from "./utils"; + +const DEFAULT_SLASH_COMMANDS_BOOKMARKS_COUNT = 5; + +const INITIAL_PREFERENCES_STATE: PreferencesState = { + bookmarkedSlashCommands: [], +}; export interface PreferencesState { - bookmarksByName: string[]; + bookmarkedSlashCommands: string[]; } export interface ProfilesState { @@ -24,21 +30,42 @@ export const profilesSlice = createSlice({ reducers: { setSelectedProfile: (state, { payload }: PayloadAction) => { state.selectedProfileId = payload; - - if (payload) { - ensureProfilePreferences(state, payload); - } }, setAvailableProfiles: ( state, { payload }: PayloadAction, ) => { state.availableProfiles = payload; + }, + initializeProfilePreferences: ( + state, + action: PayloadAction<{ + profileId: string; + defaultSlashCommands?: SlashCommandDescription[]; + }>, + ) => { + const { profileId, defaultSlashCommands = [] } = action.payload; + const defaultSlashCommandNames = defaultSlashCommands.map( + (cmd) => cmd.name, + ); - if (payload) { - for (const profile of payload) { - ensureProfilePreferences(state, profile.id); - } + // First ensure all profile preferences are complete to handle + // the case where a new preference has been added since last load + Object.keys(state.preferencesByProfileId).forEach((pid) => { + state.preferencesByProfileId[pid] = { + ...INITIAL_PREFERENCES_STATE, + ...state.preferencesByProfileId[pid], + }; + }); + + // Then initialize preferences for the new profile if needed + if (!state.preferencesByProfileId[profileId]) { + state.preferencesByProfileId[profileId] = { + bookmarkedSlashCommands: defaultSlashCommandNames.slice( + 0, + DEFAULT_SLASH_COMMANDS_BOOKMARKS_COUNT, + ), + }; } }, bookmarkSlashCommand: ( @@ -46,16 +73,15 @@ export const profilesSlice = createSlice({ action: PayloadAction<{ commandName: string }>, ) => { const { commandName } = action.payload; - const profileId = state.selectedProfileId; + const preferences = + state.preferencesByProfileId[state.selectedProfileId ?? ""]; - if (!profileId) return; + if (!preferences) return; - const bookmarks = state.preferencesByProfileId[profileId].bookmarksByName; - if (!bookmarks.includes(commandName)) { - bookmarks.push(commandName); + if (!preferences.bookmarkedSlashCommands.includes(commandName)) { + preferences.bookmarkedSlashCommands.push(commandName); } }, - unbookmarkSlashCommand: ( state, action: PayloadAction<{ commandName: string }>, @@ -66,9 +92,12 @@ export const profilesSlice = createSlice({ if (!profileId) return; const preferences = state.preferencesByProfileId[profileId]; - preferences.bookmarksByName = preferences.bookmarksByName.filter( - (cmd) => cmd !== commandName, - ); + if (!preferences) return; + + preferences.bookmarkedSlashCommands = + preferences.bookmarkedSlashCommands.filter( + (cmd) => cmd !== commandName, + ); }, }, selectors: { @@ -85,7 +114,7 @@ export const profilesSlice = createSlice({ selectBookmarkedSlashCommands: (state) => { if (!state.selectedProfileId) return []; const preferences = state.preferencesByProfileId[state.selectedProfileId]; - return preferences?.bookmarksByName || []; + return preferences?.bookmarkedSlashCommands || []; }, selectPreferencesByProfileId: (state) => state.preferencesByProfileId, @@ -97,6 +126,7 @@ export const { setSelectedProfile, bookmarkSlashCommand, unbookmarkSlashCommand, + initializeProfilePreferences, } = profilesSlice.actions; export const { diff --git a/gui/src/redux/slices/profiles/thunks.ts b/gui/src/redux/slices/profiles/thunks.ts index 7ca1094903..585e7cd2b4 100644 --- a/gui/src/redux/slices/profiles/thunks.ts +++ b/gui/src/redux/slices/profiles/thunks.ts @@ -1,7 +1,12 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; import { ProfileDescription } from "core/config/ConfigHandler"; +import { isDeprecatedCommandName } from "../../../components/ConversationStarters/utils"; import { ThunkApiType } from "../../store"; -import { setAvailableProfiles, setSelectedProfile } from "./slice"; +import { + initializeProfilePreferences, + setAvailableProfiles, + setSelectedProfile, +} from "./slice"; export const selectProfileThunk = createAsyncThunk< void, @@ -39,6 +44,7 @@ export const selectProfileThunk = createAsyncThunk< // Only update if there's a change if ((newId ?? null) !== (initialId ?? null)) { dispatch(setSelectedProfile(newId)); + extra.ideMessenger.post("didChangeSelectedProfile", { id: newId, }); @@ -83,3 +89,26 @@ export const updateProfilesThunk = createAsyncThunk< // This will trigger reselection if needed dispatch(selectProfileThunk(selectedProfileId)); }); + +export const initializeProfilePreferencesThunk = createAsyncThunk< + void, + { profileId: string }, + ThunkApiType +>( + "profiles/initializeProfilePreferences", + async (data, { getState, dispatch }) => { + const { profileId } = data; + const state = getState(); + + // Get slash commands from config + const defaultSlashCommands = + state.config.config.slashCommands.filter(isDeprecatedCommandName) ?? []; + + dispatch( + initializeProfilePreferences({ + defaultSlashCommands, + profileId, + }), + ); + }, +); diff --git a/gui/src/redux/slices/profiles/utils.ts b/gui/src/redux/slices/profiles/utils.ts deleted file mode 100644 index 58da69942d..0000000000 --- a/gui/src/redux/slices/profiles/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ProfilesState } from "./slice"; - -export function ensureProfilePreferences( - state: ProfilesState, - profileId: string, -) { - if (!state.preferencesByProfileId[profileId]) { - state.preferencesByProfileId[profileId] = { - bookmarksByName: [], - }; - } -} diff --git a/gui/src/redux/store.ts b/gui/src/redux/store.ts index 6d6c11b2b3..8aff8e1ded 100644 --- a/gui/src/redux/store.ts +++ b/gui/src/redux/store.ts @@ -62,12 +62,8 @@ const saveSubsetFilters = [ createFilter("ui", ["toolSettings", "toolGroupSettings"]), createFilter("indexing", []), createFilter("tabs", ["tabs"]), - // Add this new filter for the profiles slice - createFilter("profiles", [ - "preferencesByProfileId", - "selectedOrganizationId", - "selectedProfileId", - ]), + createFilter("organizations", ["selectedOrganizationId"]), + createFilter("profiles", ["preferencesByProfileId", "selectedProfileId"]), ]; const migrations: MigrationManifest = {