mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🧑🎨 refactor: Prompts/Sidebar styles for improved UI Consistency (#12426)
* style(sidebar): polish button styles, icon sizes, and nav separator
- Match new chat button style to sidebar toggle
- Unify icon sizes and stroke weights across sidebar icons
- Add separator between new chat and nav links
feat(prompts): inline prompt editing with route-based navigation
- Show prompts dashboard inline when editing/creating from chat
- Use route-based navigation instead of state-driven view switching
- Strip inline view to form-only without duplicate sidebar
style(prompts): unify borders, backgrounds, and admin controls
- Unify borders to border-medium and remove opaque backgrounds
- Add admin/advanced controls to sidebar
- Match chat background, move advanced toggle to versions panel
refactor(prompts): consolidate list actions into dropdown with compact layout
- Unify prompt list to single ChatGroupItem with all actions
- Consolidate actions into dropdown menu
- Distinct dropdown icons with status icon tooltips
- Remove rename action, use Trash icon matching convo items
refactor(prompts): inline-edit title with click-to-edit and save indicator
- Click title text to enter edit mode with pencil icon on hover
- Clean title input with inline save status indicator
- Uniform h-9 header height, smaller title, icon-only save status
- Remove separate edit icon button and confirm/cancel controls
style(prompts): sticky click-to-edit pill with surface-primary background
fix: preserve AuthContext identity across Vite HMR updates
createContext() was re-executed on every HMR module replacement, creating
a new context object that disconnected the existing provider from its
consumers. useAuthContext() would throw because useContext(newContext)
returned undefined while the provider was still on the old context.
Stash the context object in import.meta.hot.data so it survives HMR
re-execution. Dead-code-eliminated in production builds.
feat: add advanced prompts editor toggle to Chat settings
- Add AdvancedPrompts switch in Settings > Chat that maps the
promptsEditorMode enum (simple/advanced) to a boolean toggle
- Preserve existing behavior: switching to simple forces alwaysMakeProd
- Add focus styling to PromptName input (border-medium on focus,
outline on focus-visible, no ring)
- Add localization keys: com_nav_advanced_prompts,
com_nav_advanced_prompts_desc
- Various Prompts UI refinements
style(sidebar): update nav icons and reorder links for clarity
- Prompts: MessageSquareQuote → NotebookPen (distinct from chat bubble)
- Agent Builder: Blocks → Bot (no longer identical to Assistants)
- Memories: Database → BrainCircuit (brain + AI circuit)
- Parameters: Settings2 → SlidersHorizontal (tuning-specific)
- Reorder: Chat → Prompts → Agents → Assistants → Memories →
Bookmarks → Files → Parameters → MCP (frequency and grouping)
refactor(prompts): remove AdvancedSwitch component
The simple/advanced toggle is now in Settings > Chat, making
the standalone AdvancedSwitch redundant. Remove the component,
its barrel exports, and all render sites (PromptForm,
DashBreadcrumb, PromptsView).
style(prompts): tighten panel layout and spacing to match sidebar conventions
- Remove top margin from form wrapper, add mt-2 to header row
- Reduce header gaps and remove flex-wrap for tighter alignment
- Shrink prompt title from text-lg/h-9 to text-base/h-8
- Add pl-2 inner padding to PromptName input and static button
- Compact versions panel and deploy button container padding
- Reduce editor header and content spacing
- Compact mobile versions panel header
fix(prompts): rebuild mobile versions panel with proper transitions
- Add background (bg-surface-primary-alt) and border-l so content
doesn't bleed through
- Replace conditional render with CSS opacity/translate so both
overlay fade-out and panel slide-out animate on close
- Use inert attribute to disable focus when panel is hidden
- Replace raw × character with X icon from lucide-react
- Use size-icon button variant for close instead of text sm
- Use Tailwind classes (w-80, translate-x-full) instead of inline
style object for width and transform
- Remove unused sidePanelWidth constant
- Overlay stays in DOM with pointer-events-none when hidden
chore: remove stale tooling artifacts
Remove .bg-shell/manifest.json and .gsd symlink that were
accidentally committed — these are local dev tooling files.
fix: resolve review issues in PromptName and ChatGroupItem
- Fix Escape/onBlur race in PromptName via cancelledRef guard
- Clear existing timer before creating new one to prevent status flicker
- Guard delete mutation against empty group id
- Attach menuButtonRef to MenuButton for proper focus restoration after preview dialog closes
fix: address review findings across prompts UI and sidebar
PromptName:
- Fix Enter key double-save: set skipBlurRef before inline save to prevent
onBlur from re-firing saveName on input unmount
- Add isError prop to distinguish mutation failure from success, preventing
false 'saved' checkmark on error
- Remove dead cn ternary that always evaluated to opacity-100
- Remove unused cn import
PromptForm:
- Pass isError from updateGroupMutation to PromptName
- Remove dead default arg (= {}) on function component
ChatGroupItem:
- Stabilize handleDelete via ref pattern to prevent memo-defeating
recreation on every render (deleteGroup is a new ref each render)
- Disable delete button during mutation to prevent double-fire
ExpandedPanel:
- Restore <a> element for NewChatButton to fix middle-click (open in
new tab) regression caused by anchor-to-button migration
DashGroupItem:
- Delete dead file (no longer exported or imported anywhere)
fix: address review findings across prompts UI and sidebar
- PromptName: render error state (red X) when save fails, extract
shared commitName helper to deduplicate blur/Enter save paths
- ChatGroupItem: navigate away after deleting the active prompt in
sidebar view; use context-aware route prefix (/prompts vs /d/prompts)
for edit and card-click navigation
- InlinePromptsView: redirect to /c/new when user lacks prompts access
instead of rendering a blank screen
- Remove dead ManagePrompts component and its barrel exports (no
remaining consumers after GroupSidePanel cleanup)
fix: remove duplicate showThinking Recoil atom key
The 'showThinking' key was defined in both store/settings.ts (Recoil)
and store/showThinking.ts (Jotai). Only the Jotai atom is consumed;
the stale Recoil duplicate causes 'A key option with a unique string
value must be provided' at startup.
refactor: remove /d/prompts dashboard route and dead code
The prompts UI now lives at /prompts/* inline under the chat layout.
The old /d/prompts/* dashboard route, its layout (PromptsView), and
its breadcrumb (DashBreadcrumb) are no longer used.
- Delete PromptsView and DashBreadcrumb (zero consumers)
- Delete BackToChat button (zero consumers)
- Replace /d/prompts/* child routes with a redirect to /prompts/new
- Add /prompts index route that redirects to /prompts/new
- Update all /d/prompts navigation to /prompts:
- ChatGroupItem: always use /prompts prefix
- NoPromptGroup: navigate to /prompts
- CreatePromptForm: fallback navigate to /prompts/:id
- CreatePromptButton: simplified to /prompts/new (no dual-path)
- Strip GroupSidePanel of dashboard-only breadcrumb nav, recoil
state clearing, and useDashboardContext dependency
refactor: remove dead Dashboard code and unused translation keys
- Delete DashboardContext provider (zero remaining consumers)
- Simplify DashboardRoute layout to a plain Outlet
- Remove commented-out file/vector-store route blocks
- Change catch-all redirect from /d/files to /c/new
- Remove 7 unused translation keys (com_nav_toggle_sidebar,
com_ui_back_to_chat, com_ui_dashboard, com_ui_delete_prompt_name,
com_ui_global_group, com_ui_prompt_renamed, com_ui_rename_prompt,
com_ui_rename_prompt_name)
fix: add Babel plugin to transform import.meta.hot for Jest
`babel-plugin-transform-import-meta` handles standard properties (url,
filename, dirname, resolve) but not Vite's `hot` property. Jest runs in
CommonJS where `import.meta` is unavailable, so `import.meta.hot` in
AuthContext.tsx (added for HMR preservation) causes a SyntaxError that
breaks 27 test suites.
Add a small Babel plugin that replaces `import.meta.hot` with `undefined`
during Jest transforms, making the HMR guard blocks dead-code in tests.
fix: address review findings — inert typing, accessibility, and cleanup
- Add React type augmentation for `inert` attribute (React 18 compat)
- Replace spread hack `{...{ inert: }}` with direct prop in all 3 files
- Add `aria-hidden` to mobile overlay in PromptForm for screen readers
- Simplify deleteGroupRef pattern to direct mutation call
- Remove unused `useEffect` import and stale useMemo dependency
- Add clarifying comment for skipBlurRef mechanism in PromptName
fix: mobile UX for marketplace and prompts views
- Remove mobile new chat button from chat history section
- Add OpenSidebar entry points to marketplace and prompts views on mobile
- Move marketplace admin settings to a compact mobile top row
- Restructure prompt forms to surface category selector beside sidebar toggle
- Make versions panel slide content like the main sidebar and drop redundant borders
- Collapse versions button to icon-only on mobile
- Remove theme selector from prompt panel navigation
* fix: address PR review findings for prompts refactor
- Preserve prompt ID in /d/prompts/:id → /prompts/:id redirect
- Gate PreviewPrompt and VariableDialog behind isChatRoute to avoid
mounting dead dialogs in dashboard mode
- Add onError handler to useDeletePromptGroup and close dialog on
success
- Use useId() instead of hardcoded labelId in AdvancedPrompts
- Extract shared lazy loader for InlinePromptsView routes
* fix: complete review fixes for prompts refactor
- Move OGDialog (delete) inside isChatRoute gate with other dialogs
- Use useId() for both Switch id and label id in AdvancedPrompts
- Add com_ui_prompt_delete_error i18n key for actionable error context
- Drop no-op useCallback on handleDelete (unstable deleteGroup dep)
* refactor: hoist promptPath to module-scope constant
Eliminates stale-closure lint concern in dropdownItems useMemo and
removes the unnecessary dep array entry from onCardClick.
* refactor(sidebar): update icons and reorder links for clarity
- Replace Blocks icon with OpenAIMinimalIcon for the Assistant Builder link.
- Update Memories icon from BrainCircuit to Brain.
- Reintroduce Prompts link conditionally based on access permissions.
- Change Conversations icon from MessageSquare to MessagesSquare for consistency.
* refactor(sidebar): update icons and improve file attachment link
- Replace NewChatIcon with SquarePen in the NewChatButton for better visual consistency.
- Change AttachmentIcon to Paperclip in the file attachment link for clarity.
* refactor(sidebar): update file attachment icon for consistency
- Replace Paperclip icon with AttachmentIcon in the file attachment link for improved clarity and visual consistency.
* refactor(admin-settings): remove unused button and streamline dialog integration
- Eliminate the Admin button and its associated icon from the AdminSettings component for a cleaner interface.
- Simplify the confirm dialog integration by directly using the OGDialog without the button trigger.
* fix: context HMR issue
* style(prompts): enhance component structure and accessibility
- Update AutoSendPrompt button class for improved styling.
- Refactor List component to streamline loading and empty states.
- Ensure FilterPrompts handles context gracefully with null checks.
- Modify GroupSidePanel to prevent rendering without context.
- Simplify PromptsAccordion layout for better readability.
- Adjust CategoryIcon fallback behavior for undefined categories.
* refactor(useMCPServerManager): clean up import statements
- Remove duplicate import of MCPServerInitState for better clarity and organization.
- Adjust import order to maintain consistency with project structure.
* refactor(GroupSidePanel): restructure layout for improved readability and accessibility
- Adjust the structure of the GroupSidePanel component to enhance layout clarity.
- Move the PanelNavigation component into a more appropriate position within the hierarchy.
- Ensure consistent styling and behavior based on the isChatRoute condition.
* style(GroupSidePanel): adjust padding for improved layout consistency
- Update padding in the GroupSidePanel component to enhance visual alignment and readability.
- Ensure consistent styling across the component for a better user experience.
* fix(Conversations): add cache clearing and row height recomputation on search query change
- Implement useEffect to clear cache and recompute row heights when the search query changes.
- Enhance performance and responsiveness of the Conversations component during search operations.
* refactor(GroupSidePanel, PromptsAccordion): simplify layout and improve styling
* chore: import order
* fix: redirect users without CREATE permission from /prompts/new
Users with USE but not CREATE permission were seeing a blank page at
/prompts/new because InlinePromptsView only checked USE access.
CreatePromptForm's internal redirect was bypassed by the onSuccess
prop always being passed. Add CREATE check in InlinePromptsView so
the redirect happens before CreatePromptForm mounts.
* fix: restore dropdown actions for all routes and handle non-creator landing
- Remove isChatRoute gate on dropdown menu so preview, edit, and
delete actions are available on the prompts management route
- Un-gate PreviewPrompt and OGDialog (delete) since both are
triggered from the now-always-visible dropdown
- Keep VariableDialog gated behind isChatRoute (chat submission only)
- Show EmptyPromptPreview for non-creators at /prompts/new instead
of redirecting to /c/new, so they stay in the prompts section
with sidebar access to browse existing prompts
* fix: add isPublic to TPromptGroup type
The database schema (IPromptGroup in data-schemas) has isPublic but
the shared TPromptGroup type in data-provider was missing it,
causing a TS2339 error in ChatGroupItem.
* fix: prevent duplicate rename and restore name on error in PromptName
- Block re-entry to edit mode while a save is in flight by guarding
the click handler with isLoading/saveStatus checks
- Reset newName to the prop value when mutation fails so the UI
doesn't display the unsaved name after the error icon clears
* fix: address review findings across prompts refactor
- Consolidate duplicate usePromptGroupsContext() calls in PromptForm
- Remove invalid aria-labelledby (text string, not ID) from
AutoSendPrompt checkbox that is already aria-hidden
- Remove useMemo wrapping trivial `disabled ?? false` in ToolsDropdown
- Remove dead context spread in PromptsAccordion (GroupSidePanel
reads context internally)
- Wrap search cache-clear effect in requestAnimationFrame to match
favorites effect pattern in Conversations
- Use Set for O(1) lookups in MCPSelect server filtering
- Fix unnecessary JSX expression wrapper on string literal in
CreatePromptButton Link
* style(PromptTextCard): update icon classes for improved accessibility
- Add 'text-text-secondary' class to Check and Copy icons for better visibility and consistency in the PromptTextCard component.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
55fc37ff49
commit
b8158a613e
72 changed files with 936 additions and 1416 deletions
|
|
@ -14,6 +14,7 @@ module.exports = {
|
|||
*/
|
||||
plugins: [
|
||||
'@babel/plugin-transform-runtime',
|
||||
'./test/babel-plugin-transform-import-meta-hot.cjs',
|
||||
'babel-plugin-transform-import-meta',
|
||||
'babel-plugin-transform-vite-meta-env',
|
||||
'babel-plugin-replace-ts-export-assignment',
|
||||
|
|
|
|||
|
|
@ -29,11 +29,7 @@ interface BadgeRowContextType {
|
|||
const BadgeRowContext = createContext<BadgeRowContextType | undefined>(undefined);
|
||||
|
||||
export function useBadgeRowContext() {
|
||||
const context = useContext(BadgeRowContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useBadgeRowContext must be used within a BadgeRowProvider');
|
||||
}
|
||||
return context;
|
||||
return useContext(BadgeRowContext);
|
||||
}
|
||||
|
||||
interface BadgeRowProviderProps {
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
type TDashboardContext = {
|
||||
prevLocationPath: string;
|
||||
};
|
||||
|
||||
export const DashboardContext = createContext<TDashboardContext>({} as TDashboardContext);
|
||||
export const useDashboardContext = () => useContext(DashboardContext);
|
||||
|
|
@ -83,10 +83,14 @@ export function DragDropProvider({ children }: { children: React.ReactNode }) {
|
|||
return <DragDropContext.Provider value={contextValue}>{children}</DragDropContext.Provider>;
|
||||
}
|
||||
|
||||
const defaultDragDropValue: DragDropContextValue = {
|
||||
conversationId: undefined,
|
||||
agentId: undefined,
|
||||
endpoint: undefined,
|
||||
endpointType: undefined,
|
||||
useResponsesApi: undefined,
|
||||
};
|
||||
|
||||
export function useDragDropContext() {
|
||||
const context = useContext(DragDropContext);
|
||||
if (!context) {
|
||||
throw new Error('useDragDropContext must be used within DragDropProvider');
|
||||
}
|
||||
return context;
|
||||
return useContext(DragDropContext) ?? defaultDragDropValue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,9 +77,5 @@ export const PromptGroupsProvider = ({ children }: { children: ReactNode }) => {
|
|||
};
|
||||
|
||||
export const usePromptGroupsContext = () => {
|
||||
const context = useContext(PromptGroupsContext);
|
||||
if (!context) {
|
||||
throw new Error('usePromptGroupsContext must be used within a PromptGroupsProvider');
|
||||
}
|
||||
return context;
|
||||
return useContext(PromptGroupsContext);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ export * from './EditorContext';
|
|||
export * from './ChatFormContext';
|
||||
export * from './BookmarkContext';
|
||||
export * from './MessageContext';
|
||||
export * from './DashboardContext';
|
||||
export * from './AssistantsContext';
|
||||
export * from './AgentsContext';
|
||||
export * from './AssistantsMapContext';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
||||
import { useSearchParams, useParams, useNavigate } from 'react-router-dom';
|
||||
import { useMediaQuery } from '@librechat/client';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import { useSearchParams, useParams, useNavigate } from 'react-router-dom';
|
||||
import type t from 'librechat-data-provider';
|
||||
import { useDocumentTitle, useHasAccess, useLocalize, TranslationKeys } from '~/hooks';
|
||||
import { useGetEndpointsQuery, useGetAgentCategoriesQuery } from '~/data-provider';
|
||||
import MarketplaceAdminSettings from './MarketplaceAdminSettings';
|
||||
import OpenSidebar from '~/components/Chat/Menus/OpenSidebar';
|
||||
import { SidePanelGroup } from '~/components/SidePanel';
|
||||
import CategoryTabs from './CategoryTabs';
|
||||
import SearchBar from './SearchBar';
|
||||
|
|
@ -215,14 +216,19 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
|||
</div>
|
||||
)}
|
||||
{/* Sticky wrapper for search bar and categories */}
|
||||
<div className="sticky top-0 z-10 bg-presentation pb-4">
|
||||
<div className="sticky top-0 z-10 mt-4 bg-presentation pb-4 md:mt-0">
|
||||
<div className="container mx-auto max-w-4xl px-4">
|
||||
<div className="mx-auto mb-3 flex max-w-2xl items-center justify-between gap-2 md:hidden">
|
||||
<OpenSidebar />
|
||||
<MarketplaceAdminSettings compact />
|
||||
</div>
|
||||
{/* Search bar */}
|
||||
<div className="mx-auto flex max-w-2xl gap-2 pb-6">
|
||||
<SearchBar value={searchQuery} onSearch={handleSearch} />
|
||||
{/* TODO: Remove this once we have a better way to handle admin settings */}
|
||||
{/* Admin Settings */}
|
||||
<MarketplaceAdminSettings />
|
||||
<div className="hidden md:block">
|
||||
<MarketplaceAdminSettings />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category tabs */}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const permissions: PermissionConfig[] = [
|
|||
{ permission: Permissions.USE, labelKey: 'com_ui_marketplace_allow_use' },
|
||||
];
|
||||
|
||||
const MarketplaceAdminSettings = () => {
|
||||
const MarketplaceAdminSettings = ({ compact = false }: { compact?: boolean }) => {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
|
|
@ -23,7 +23,16 @@ const MarketplaceAdminSettings = () => {
|
|||
},
|
||||
});
|
||||
|
||||
const trigger = (
|
||||
const trigger = compact ? (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="rounded-xl bg-presentation duration-0 hover:bg-surface-active-alt"
|
||||
aria-label={localize('com_ui_admin_settings')}
|
||||
>
|
||||
<ShieldEllipsis className="icon-md" aria-hidden="true" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="relative h-12 rounded-xl border-border-medium font-medium"
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ interface ArtifactsToggleState {
|
|||
|
||||
function Artifacts() {
|
||||
const localize = useLocalize();
|
||||
const { artifacts } = useBadgeRowContext();
|
||||
const { toggleState, debouncedChange, isPinned } = artifacts;
|
||||
const context = useBadgeRowContext();
|
||||
const { toggleState, debouncedChange, isPinned } = context?.artifacts ?? {};
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [isButtonExpanded, setIsButtonExpanded] = useState(false);
|
||||
|
|
@ -32,6 +32,7 @@ function Artifacts() {
|
|||
const isCustomEnabled = currentState.mode === ArtifactModes.CUSTOM;
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (!debouncedChange) return;
|
||||
if (isEnabled) {
|
||||
debouncedChange({ value: '' });
|
||||
setIsButtonExpanded(false);
|
||||
|
|
@ -55,6 +56,7 @@ function Artifacts() {
|
|||
}, [isPopoverOpen]);
|
||||
|
||||
const handleShadcnToggle = useCallback(() => {
|
||||
if (!debouncedChange) return;
|
||||
if (isShadcnEnabled) {
|
||||
debouncedChange({ value: ArtifactModes.DEFAULT });
|
||||
} else {
|
||||
|
|
@ -63,6 +65,7 @@ function Artifacts() {
|
|||
}, [isShadcnEnabled, debouncedChange]);
|
||||
|
||||
const handleCustomToggle = useCallback(() => {
|
||||
if (!debouncedChange) return;
|
||||
if (isCustomEnabled) {
|
||||
debouncedChange({ value: ArtifactModes.DEFAULT });
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { useBadgeRowContext } from '~/Providers';
|
|||
|
||||
function CodeInterpreter() {
|
||||
const localize = useLocalize();
|
||||
const { codeInterpreter, codeApiKeyForm } = useBadgeRowContext();
|
||||
const { toggleState: runCode, debouncedChange, isPinned } = codeInterpreter;
|
||||
const { badgeTriggerRef } = codeApiKeyForm;
|
||||
const context = useBadgeRowContext();
|
||||
const { toggleState: runCode, debouncedChange, isPinned } = context?.codeInterpreter ?? {};
|
||||
const { badgeTriggerRef } = context?.codeApiKeyForm ?? {};
|
||||
|
||||
const canRunCode = useHasAccess({
|
||||
permissionType: PermissionTypes.RUN_CODE,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { useBadgeRowContext } from '~/Providers';
|
|||
|
||||
function FileSearch() {
|
||||
const localize = useLocalize();
|
||||
const { fileSearch } = useBadgeRowContext();
|
||||
const { toggleState: fileSearchEnabled, debouncedChange, isPinned } = fileSearch;
|
||||
const context = useBadgeRowContext();
|
||||
const { toggleState: fileSearchEnabled, debouncedChange, isPinned } = context?.fileSearch ?? {};
|
||||
|
||||
const canUseFileSearch = useHasAccess({
|
||||
permissionType: PermissionTypes.FILE_SEARCH,
|
||||
|
|
|
|||
|
|
@ -6,47 +6,55 @@ import { TooltipAnchor } from '@librechat/client';
|
|||
import MCPServerMenuItem from '~/components/MCP/MCPServerMenuItem';
|
||||
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
|
||||
import StackedMCPIcons from '~/components/MCP/StackedMCPIcons';
|
||||
import { useHasAccess, useLocalize } from '~/hooks';
|
||||
import { useBadgeRowContext } from '~/Providers';
|
||||
import { useHasAccess } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
function MCPSelectContent() {
|
||||
const { conversationId, storageContextKey, mcpServerManager } = useBadgeRowContext();
|
||||
const {
|
||||
localize,
|
||||
isPinned,
|
||||
mcpValues,
|
||||
placeholderText,
|
||||
selectableServers,
|
||||
connectionStatus,
|
||||
isInitializing,
|
||||
getConfigDialogProps,
|
||||
toggleServerSelection,
|
||||
getServerStatusIconProps,
|
||||
} = mcpServerManager;
|
||||
const localize = useLocalize();
|
||||
const context = useBadgeRowContext();
|
||||
const { conversationId, storageContextKey, mcpServerManager: manager } = context ?? {};
|
||||
|
||||
const menuStore = Ariakit.useMenuStore({ focusLoop: true });
|
||||
const isOpen = menuStore.useState('open');
|
||||
|
||||
const selectedCount = mcpValues?.length ?? 0;
|
||||
|
||||
const selectedServers = useMemo(() => {
|
||||
if (!mcpValues || mcpValues.length === 0) {
|
||||
if (!manager?.mcpValues || manager.mcpValues.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return selectableServers.filter((s) => mcpValues.includes(s.serverName));
|
||||
}, [selectableServers, mcpValues]);
|
||||
const selectedSet = new Set(manager.mcpValues);
|
||||
return manager.selectableServers?.filter((s) => selectedSet.has(s.serverName));
|
||||
}, [manager?.selectableServers, manager?.mcpValues]);
|
||||
|
||||
const displayText = useMemo(() => {
|
||||
const selectedCount = manager?.mcpValues?.length ?? 0;
|
||||
if (selectedCount === 0) {
|
||||
return null;
|
||||
}
|
||||
if (selectedCount === 1) {
|
||||
const server = selectableServers.find((s) => s.serverName === mcpValues?.[0]);
|
||||
return server?.config?.title || mcpValues?.[0];
|
||||
const server = manager?.selectableServers?.find(
|
||||
(s) => s.serverName === manager?.mcpValues?.[0],
|
||||
);
|
||||
return server?.config?.title || manager?.mcpValues?.[0];
|
||||
}
|
||||
return localize('com_ui_x_selected', { 0: selectedCount });
|
||||
}, [selectedCount, selectableServers, mcpValues, localize]);
|
||||
}, [manager?.selectableServers, manager?.mcpValues, localize]);
|
||||
|
||||
if (!manager) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
isPinned,
|
||||
mcpValues,
|
||||
isInitializing,
|
||||
placeholderText,
|
||||
connectionStatus,
|
||||
selectableServers,
|
||||
getConfigDialogProps,
|
||||
toggleServerSelection,
|
||||
getServerStatusIconProps,
|
||||
} = manager;
|
||||
|
||||
if (!isPinned && mcpValues?.length === 0) {
|
||||
return null;
|
||||
|
|
@ -126,8 +134,8 @@ function MCPSelectContent() {
|
|||
}
|
||||
|
||||
function MCPSelect() {
|
||||
const { mcpServerManager } = useBadgeRowContext();
|
||||
const { selectableServers } = mcpServerManager;
|
||||
const context = useBadgeRowContext();
|
||||
const { selectableServers } = context?.mcpServerManager ?? {};
|
||||
const canUseMcp = useHasAccess({
|
||||
permissionType: PermissionTypes.MCP_SERVERS,
|
||||
permission: Permissions.USE,
|
||||
|
|
|
|||
|
|
@ -15,19 +15,8 @@ interface MCPSubMenuProps {
|
|||
const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
|
||||
({ placeholder, ...props }, ref) => {
|
||||
const localize = useLocalize();
|
||||
const { storageContextKey, mcpServerManager } = useBadgeRowContext();
|
||||
const {
|
||||
isPinned,
|
||||
mcpValues,
|
||||
setIsPinned,
|
||||
placeholderText,
|
||||
selectableServers,
|
||||
connectionStatus,
|
||||
isInitializing,
|
||||
getConfigDialogProps,
|
||||
toggleServerSelection,
|
||||
getServerStatusIconProps,
|
||||
} = mcpServerManager;
|
||||
const context = useBadgeRowContext();
|
||||
const { storageContextKey, mcpServerManager } = context ?? {};
|
||||
|
||||
const menuStore = Ariakit.useMenuStore({
|
||||
focusLoop: true,
|
||||
|
|
@ -35,6 +24,23 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
|
|||
placement: 'right',
|
||||
});
|
||||
|
||||
if (!mcpServerManager) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
isPinned,
|
||||
mcpValues,
|
||||
setIsPinned,
|
||||
isInitializing,
|
||||
placeholderText,
|
||||
connectionStatus,
|
||||
selectableServers,
|
||||
getConfigDialogProps,
|
||||
toggleServerSelection,
|
||||
getServerStatusIconProps,
|
||||
} = mcpServerManager;
|
||||
|
||||
if (!selectableServers || selectableServers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,8 +62,9 @@ function PromptsCommand({
|
|||
}) {
|
||||
const localize = useLocalize();
|
||||
const { mutate: recordUsage } = useRecordPromptUsage();
|
||||
const { allPromptGroups, hasAccess } = usePromptGroupsContext();
|
||||
const { data, isLoading } = allPromptGroups;
|
||||
const promptGroupsContext = usePromptGroupsContext();
|
||||
const { allPromptGroups, hasAccess } = promptGroupsContext ?? {};
|
||||
const { data, isLoading } = allPromptGroups ?? {};
|
||||
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
|
|
|||
|
|
@ -5,9 +5,19 @@ import CodeApiKeyDialog from '~/components/SidePanel/Agents/Code/ApiKeyDialog';
|
|||
import { useBadgeRowContext } from '~/Providers';
|
||||
|
||||
function ToolDialogs() {
|
||||
const { webSearch, codeInterpreter, searchApiKeyForm, codeApiKeyForm } = useBadgeRowContext();
|
||||
const { authData: webSearchAuthData } = webSearch;
|
||||
const { authData: codeAuthData } = codeInterpreter;
|
||||
const context = useBadgeRowContext();
|
||||
const { webSearch, codeInterpreter, searchApiKeyForm, codeApiKeyForm } = context ?? {};
|
||||
const { authData: webSearchAuthData } = webSearch ?? {};
|
||||
const { authData: codeAuthData } = codeInterpreter ?? {};
|
||||
const searchAuthTypes = useMemo(
|
||||
() => webSearchAuthData?.authTypes ?? [],
|
||||
[webSearchAuthData?.authTypes],
|
||||
);
|
||||
const codeAuthType = useMemo(() => codeAuthData?.message ?? false, [codeAuthData?.message]);
|
||||
|
||||
if (!searchApiKeyForm || !codeApiKeyForm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
methods: searchMethods,
|
||||
|
|
@ -29,12 +39,6 @@ function ToolDialogs() {
|
|||
menuTriggerRef: codeMenuTriggerRef,
|
||||
} = codeApiKeyForm;
|
||||
|
||||
const searchAuthTypes = useMemo(
|
||||
() => webSearchAuthData?.authTypes ?? [],
|
||||
[webSearchAuthData?.authTypes],
|
||||
);
|
||||
const codeAuthType = useMemo(() => codeAuthData?.message ?? false, [codeAuthData?.message]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchApiKeyDialog
|
||||
|
|
|
|||
|
|
@ -23,39 +23,11 @@ interface ToolsDropdownProps {
|
|||
|
||||
const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
||||
const localize = useLocalize();
|
||||
const isDisabled = disabled ?? false;
|
||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||
const {
|
||||
webSearch,
|
||||
artifacts,
|
||||
fileSearch,
|
||||
agentsConfig,
|
||||
mcpServerManager,
|
||||
codeApiKeyForm,
|
||||
codeInterpreter,
|
||||
searchApiKeyForm,
|
||||
} = useBadgeRowContext();
|
||||
const context = useBadgeRowContext();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
||||
const { codeEnabled, webSearchEnabled, artifactsEnabled, fileSearchEnabled } =
|
||||
useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities);
|
||||
|
||||
const { setIsDialogOpen: setIsCodeDialogOpen, menuTriggerRef: codeMenuTriggerRef } =
|
||||
codeApiKeyForm;
|
||||
const { setIsDialogOpen: setIsSearchDialogOpen, menuTriggerRef: searchMenuTriggerRef } =
|
||||
searchApiKeyForm;
|
||||
const {
|
||||
isPinned: isSearchPinned,
|
||||
setIsPinned: setIsSearchPinned,
|
||||
authData: webSearchAuthData,
|
||||
} = webSearch;
|
||||
const {
|
||||
isPinned: isCodePinned,
|
||||
setIsPinned: setIsCodePinned,
|
||||
authData: codeAuthData,
|
||||
} = codeInterpreter;
|
||||
const { isPinned: isFileSearchPinned, setIsPinned: setIsFileSearchPinned } = fileSearch;
|
||||
const { isPinned: isArtifactsPinned, setIsPinned: setIsArtifactsPinned } = artifacts;
|
||||
useAgentCapabilities(context?.agentsConfig?.capabilities ?? defaultAgentCapabilities);
|
||||
|
||||
const canUseWebSearch = useHasAccess({
|
||||
permissionType: PermissionTypes.WEB_SEARCH,
|
||||
|
|
@ -77,6 +49,35 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
|||
permission: Permissions.USE,
|
||||
});
|
||||
|
||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||
const isDisabled = disabled ?? false;
|
||||
const {
|
||||
webSearch,
|
||||
artifacts,
|
||||
fileSearch,
|
||||
mcpServerManager,
|
||||
codeApiKeyForm,
|
||||
codeInterpreter,
|
||||
searchApiKeyForm,
|
||||
} = context ?? {};
|
||||
|
||||
const { setIsDialogOpen: setIsCodeDialogOpen, menuTriggerRef: codeMenuTriggerRef } =
|
||||
codeApiKeyForm ?? {};
|
||||
const { setIsDialogOpen: setIsSearchDialogOpen, menuTriggerRef: searchMenuTriggerRef } =
|
||||
searchApiKeyForm ?? {};
|
||||
const {
|
||||
isPinned: isSearchPinned,
|
||||
setIsPinned: setIsSearchPinned,
|
||||
authData: webSearchAuthData,
|
||||
} = webSearch ?? {};
|
||||
const {
|
||||
isPinned: isCodePinned,
|
||||
setIsPinned: setIsCodePinned,
|
||||
authData: codeAuthData,
|
||||
} = codeInterpreter ?? {};
|
||||
const { isPinned: isFileSearchPinned, setIsPinned: setIsFileSearchPinned } = fileSearch ?? {};
|
||||
const { isPinned: isArtifactsPinned, setIsPinned: setIsArtifactsPinned } = artifacts ?? {};
|
||||
|
||||
const showWebSearchSettings = useMemo(() => {
|
||||
const authTypes = webSearchAuthData?.authTypes ?? [];
|
||||
if (authTypes.length === 0) return true;
|
||||
|
|
@ -89,44 +90,44 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
|||
);
|
||||
|
||||
const handleWebSearchToggle = useCallback(() => {
|
||||
const newValue = !webSearch.toggleState;
|
||||
webSearch.debouncedChange({ value: newValue });
|
||||
const newValue = !webSearch?.toggleState;
|
||||
webSearch?.debouncedChange({ value: newValue });
|
||||
}, [webSearch]);
|
||||
|
||||
const handleCodeInterpreterToggle = useCallback(() => {
|
||||
const newValue = !codeInterpreter.toggleState;
|
||||
codeInterpreter.debouncedChange({ value: newValue });
|
||||
const newValue = !codeInterpreter?.toggleState;
|
||||
codeInterpreter?.debouncedChange({ value: newValue });
|
||||
}, [codeInterpreter]);
|
||||
|
||||
const handleFileSearchToggle = useCallback(() => {
|
||||
const newValue = !fileSearch.toggleState;
|
||||
fileSearch.debouncedChange({ value: newValue });
|
||||
const newValue = !fileSearch?.toggleState;
|
||||
fileSearch?.debouncedChange({ value: newValue });
|
||||
}, [fileSearch]);
|
||||
|
||||
const handleArtifactsToggle = useCallback(() => {
|
||||
const currentState = artifacts.toggleState;
|
||||
const currentState = artifacts?.toggleState;
|
||||
if (!currentState || currentState === '') {
|
||||
artifacts.debouncedChange({ value: ArtifactModes.DEFAULT });
|
||||
artifacts?.debouncedChange({ value: ArtifactModes.DEFAULT });
|
||||
} else {
|
||||
artifacts.debouncedChange({ value: '' });
|
||||
artifacts?.debouncedChange({ value: '' });
|
||||
}
|
||||
}, [artifacts]);
|
||||
|
||||
const handleShadcnToggle = useCallback(() => {
|
||||
const currentState = artifacts.toggleState;
|
||||
const currentState = artifacts?.toggleState;
|
||||
if (currentState === ArtifactModes.SHADCNUI) {
|
||||
artifacts.debouncedChange({ value: ArtifactModes.DEFAULT });
|
||||
artifacts?.debouncedChange({ value: ArtifactModes.DEFAULT });
|
||||
} else {
|
||||
artifacts.debouncedChange({ value: ArtifactModes.SHADCNUI });
|
||||
artifacts?.debouncedChange({ value: ArtifactModes.SHADCNUI });
|
||||
}
|
||||
}, [artifacts]);
|
||||
|
||||
const handleCustomToggle = useCallback(() => {
|
||||
const currentState = artifacts.toggleState;
|
||||
const currentState = artifacts?.toggleState;
|
||||
if (currentState === ArtifactModes.CUSTOM) {
|
||||
artifacts.debouncedChange({ value: ArtifactModes.DEFAULT });
|
||||
artifacts?.debouncedChange({ value: ArtifactModes.DEFAULT });
|
||||
} else {
|
||||
artifacts.debouncedChange({ value: ArtifactModes.CUSTOM });
|
||||
artifacts?.debouncedChange({ value: ArtifactModes.CUSTOM });
|
||||
}
|
||||
}, [artifacts]);
|
||||
|
||||
|
|
@ -148,7 +149,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
|||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsFileSearchPinned(!isFileSearchPinned);
|
||||
setIsFileSearchPinned?.(!isFileSearchPinned);
|
||||
}}
|
||||
className={cn(
|
||||
'rounded p-1 transition-all duration-200',
|
||||
|
|
@ -182,7 +183,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
|||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsSearchDialogOpen(true);
|
||||
setIsSearchDialogOpen?.(true);
|
||||
}}
|
||||
className={cn(
|
||||
'rounded p-1 transition-all duration-200',
|
||||
|
|
@ -201,7 +202,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
|||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsSearchPinned(!isSearchPinned);
|
||||
setIsSearchPinned?.(!isSearchPinned);
|
||||
}}
|
||||
className={cn(
|
||||
'rounded p-1 transition-all duration-200',
|
||||
|
|
@ -236,7 +237,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
|||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsCodeDialogOpen(true);
|
||||
setIsCodeDialogOpen?.(true);
|
||||
}}
|
||||
ref={codeMenuTriggerRef}
|
||||
className={cn(
|
||||
|
|
@ -255,7 +256,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
|||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsCodePinned(!isCodePinned);
|
||||
setIsCodePinned?.(!isCodePinned);
|
||||
}}
|
||||
className={cn(
|
||||
'rounded p-1 transition-all duration-200',
|
||||
|
|
@ -274,15 +275,15 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
|||
});
|
||||
}
|
||||
|
||||
if (artifactsEnabled) {
|
||||
if (artifactsEnabled && setIsArtifactsPinned != null) {
|
||||
dropdownItems.push({
|
||||
hideOnClick: false,
|
||||
render: (props) => (
|
||||
<ArtifactsSubMenu
|
||||
{...props}
|
||||
isArtifactsPinned={isArtifactsPinned}
|
||||
isArtifactsPinned={isArtifactsPinned ?? false}
|
||||
setIsArtifactsPinned={setIsArtifactsPinned}
|
||||
artifactsMode={artifacts.toggleState as string}
|
||||
artifactsMode={artifacts?.toggleState as string}
|
||||
handleArtifactsToggle={handleArtifactsToggle}
|
||||
handleShadcnToggle={handleShadcnToggle}
|
||||
handleCustomToggle={handleCustomToggle}
|
||||
|
|
@ -291,7 +292,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
|||
});
|
||||
}
|
||||
|
||||
const { availableMCPServers } = mcpServerManager;
|
||||
const { availableMCPServers } = mcpServerManager ?? {};
|
||||
if (canUseMcp && availableMCPServers && availableMCPServers.length > 0) {
|
||||
dropdownItems.push({
|
||||
hideOnClick: false,
|
||||
|
|
|
|||
|
|
@ -7,18 +7,20 @@ import { useBadgeRowContext } from '~/Providers';
|
|||
|
||||
function WebSearch() {
|
||||
const localize = useLocalize();
|
||||
const { webSearch: webSearchData, searchApiKeyForm } = useBadgeRowContext();
|
||||
const { toggleState: webSearch, debouncedChange, isPinned, authData } = webSearchData;
|
||||
const { badgeTriggerRef } = searchApiKeyForm;
|
||||
|
||||
const canUseWebSearch = useHasAccess({
|
||||
permissionType: PermissionTypes.WEB_SEARCH,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
|
||||
const context = useBadgeRowContext();
|
||||
if (!canUseWebSearch) {
|
||||
return null;
|
||||
}
|
||||
if (!context) {
|
||||
return null;
|
||||
}
|
||||
const { webSearch: webSearchData, searchApiKeyForm } = context;
|
||||
const { toggleState: webSearch, debouncedChange, isPinned, authData } = webSearchData;
|
||||
const { badgeTriggerRef } = searchApiKeyForm;
|
||||
|
||||
return (
|
||||
(isPinned || (webSearch && authData?.authenticated)) && (
|
||||
|
|
|
|||
|
|
@ -266,6 +266,16 @@ const Conversations: FC<ConversationsProps> = ({
|
|||
return () => cancelAnimationFrame(frameId);
|
||||
}, [favorites.length, isFavoritesLoading, showAgentMarketplace, clearFavoritesCache]);
|
||||
|
||||
useEffect(() => {
|
||||
const frameId = requestAnimationFrame(() => {
|
||||
cache.clearAll();
|
||||
if (containerRef.current && 'recomputeRowHeights' in containerRef.current) {
|
||||
containerRef.current.recomputeRowHeights(0);
|
||||
}
|
||||
});
|
||||
return () => cancelAnimationFrame(frameId);
|
||||
}, [search.query, cache, containerRef]);
|
||||
|
||||
const rowRenderer = useCallback(
|
||||
({ index, key, parent, style }) => {
|
||||
const item = flattenedItems[index];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import { useCallback, useId } from 'react';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { Switch, InfoHoverCard, ESide } from '@librechat/client';
|
||||
import { PromptsEditorMode } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
const { promptsEditorMode, alwaysMakeProd } = store;
|
||||
|
||||
export default function AdvancedPrompts() {
|
||||
const localize = useLocalize();
|
||||
const [mode, setMode] = useRecoilState(promptsEditorMode);
|
||||
const setAlwaysMakeProd = useSetRecoilState(alwaysMakeProd);
|
||||
|
||||
const isAdvanced = mode === PromptsEditorMode.ADVANCED;
|
||||
|
||||
const handleChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
if (!checked) {
|
||||
setAlwaysMakeProd(true);
|
||||
}
|
||||
setMode(checked ? PromptsEditorMode.ADVANCED : PromptsEditorMode.SIMPLE);
|
||||
},
|
||||
[setMode, setAlwaysMakeProd],
|
||||
);
|
||||
|
||||
const rootId = useId();
|
||||
const labelId = `${rootId}-label`;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div id={labelId}>{localize('com_nav_advanced_prompts')}</div>
|
||||
<InfoHoverCard side={ESide.Bottom} text={localize('com_nav_advanced_prompts_desc')} />
|
||||
</div>
|
||||
<Switch
|
||||
id={rootId}
|
||||
checked={isAdvanced}
|
||||
onCheckedChange={handleChange}
|
||||
className="ml-4"
|
||||
data-testid="advancedPrompts"
|
||||
aria-labelledby={labelId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { memo } from 'react';
|
||||
import { showThinkingAtom } from '~/store/showThinking';
|
||||
import AdvancedPrompts from './AdvancedPrompts';
|
||||
import FontSizeSelector from './FontSizeSelector';
|
||||
import { ForkSettings } from './ForkSettings';
|
||||
import ChatDirection from './ChatDirection';
|
||||
|
|
@ -119,6 +120,9 @@ function Chat() {
|
|||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="pb-3">
|
||||
<AdvancedPrompts />
|
||||
</div>
|
||||
<ForkSettings />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { ShieldEllipsis } from 'lucide-react';
|
||||
import { Permissions, PermissionTypes } from 'librechat-data-provider';
|
||||
import { OGDialog, OGDialogTemplate, Button, useToastContext } from '@librechat/client';
|
||||
import { OGDialog, OGDialogTemplate, useToastContext } from '@librechat/client';
|
||||
import type { PermissionConfig } from '~/components/ui';
|
||||
import { useUpdatePromptPermissionsMutation } from '~/data-provider';
|
||||
import { AdminSettingsDialog } from '~/components/ui';
|
||||
|
|
@ -39,18 +38,6 @@ const AdminSettings = () => {
|
|||
setConfirmAdminUseChange({ newValue, callback: onChange });
|
||||
};
|
||||
|
||||
const trigger = (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
aria-label={localize('com_ui_admin')}
|
||||
className="mr-2 h-10 w-fit sm:m-0"
|
||||
>
|
||||
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
|
||||
<span className="hidden sm:flex">{localize('com_ui_admin')}</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const confirmDialog = (
|
||||
<OGDialog
|
||||
open={confirmAdminUseChange !== null}
|
||||
|
|
@ -88,7 +75,6 @@ const AdminSettings = () => {
|
|||
permissions={permissions}
|
||||
menuId="prompt-role-dropdown"
|
||||
mutation={mutation}
|
||||
trigger={trigger}
|
||||
onPermissionConfirm={handlePermissionConfirm}
|
||||
confirmPermissions={[Permissions.USE]}
|
||||
extraContent={confirmDialog}
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { Sparkles, Layers } from 'lucide-react';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { PromptsEditorMode } from '~/common';
|
||||
import { Radio } from '@librechat/client';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
const { promptsEditorMode, alwaysMakeProd } = store;
|
||||
|
||||
const AdvancedSwitch = () => {
|
||||
const localize = useLocalize();
|
||||
const [mode, setMode] = useRecoilState(promptsEditorMode);
|
||||
const setAlwaysMakeProd = useSetRecoilState(alwaysMakeProd);
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: PromptsEditorMode.SIMPLE,
|
||||
label: localize('com_ui_simple'),
|
||||
icon: <Sparkles className="size-3.5" />,
|
||||
},
|
||||
{
|
||||
value: PromptsEditorMode.ADVANCED,
|
||||
label: localize('com_ui_advanced'),
|
||||
icon: <Layers className="size-3.5" />,
|
||||
},
|
||||
],
|
||||
[localize],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
if (value === PromptsEditorMode.SIMPLE) {
|
||||
setAlwaysMakeProd(true);
|
||||
}
|
||||
setMode(value as PromptsEditorMode);
|
||||
},
|
||||
[setMode, setAlwaysMakeProd],
|
||||
);
|
||||
|
||||
return (
|
||||
<Radio
|
||||
options={options}
|
||||
value={mode}
|
||||
onChange={handleChange}
|
||||
className="border border-border-light"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedSwitch;
|
||||
|
|
@ -25,13 +25,13 @@ export default function AutoSendPrompt({
|
|||
onClick={() => handleCheckedChange(!autoSendPrompts)}
|
||||
aria-label={localize('com_nav_auto_send_prompts')}
|
||||
aria-pressed={autoSendPrompts}
|
||||
className={autoSendPrompts ? 'bg-surface-hover hover:bg-surface-hover' : ''}
|
||||
className={`relative h-9 w-full gap-2 rounded-lg border-border-light font-medium ${autoSendPrompts ? 'bg-surface-hover hover:bg-surface-hover' : ''}`}
|
||||
>
|
||||
<Checkbox
|
||||
checked={autoSendPrompts}
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none mr-2"
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
{localize('com_nav_auto_send_prompts')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { buttonVariants } from '@librechat/client';
|
||||
import { useDashboardContext } from '~/Providers';
|
||||
import { useLocalize, useCustomLink } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function BackToChat({ className }: { className?: string }) {
|
||||
const localize = useLocalize();
|
||||
const { prevLocationPath } = useDashboardContext();
|
||||
|
||||
const conversationId = useMemo(() => {
|
||||
if (!prevLocationPath || prevLocationPath.includes('/d/')) {
|
||||
return 'new';
|
||||
}
|
||||
const parts = prevLocationPath.split('/');
|
||||
return parts[parts.length - 1];
|
||||
}, [prevLocationPath]);
|
||||
|
||||
const href = `/c/${conversationId}`;
|
||||
const clickHandler = useCustomLink(href);
|
||||
|
||||
return (
|
||||
<a
|
||||
className={cn(buttonVariants({ variant: 'outline' }), className)}
|
||||
href={href}
|
||||
onClick={clickHandler}
|
||||
>
|
||||
<ArrowLeft className="icon-xs mr-2" aria-hidden="true" />
|
||||
{localize('com_ui_back_to_chat')}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ export default function CreatePromptButton() {
|
|||
className="size-9 shrink-0 bg-transparent"
|
||||
aria-label={localize('com_ui_create_prompt')}
|
||||
>
|
||||
<Link to="/d/prompts/new">
|
||||
<Link to="/prompts/new">
|
||||
<Plus className="size-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { Button } from '@librechat/client';
|
||||
import { useLocalize, useCustomLink } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
export default function ManagePrompts({ className }: { className?: string }) {
|
||||
const localize = useLocalize();
|
||||
const setPromptsName = useSetRecoilState(store.promptsName);
|
||||
const setPromptsCategory = useSetRecoilState(store.promptsCategory);
|
||||
const clickCallback = useCallback(() => {
|
||||
setPromptsName('');
|
||||
setPromptsCategory('');
|
||||
}, [setPromptsName, setPromptsCategory]);
|
||||
|
||||
const customLink = useCustomLink('/d/prompts', clickCallback);
|
||||
const clickHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
customLink(e as unknown as React.MouseEvent<HTMLAnchorElement>);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(className, 'bg-transparent')}
|
||||
onClick={clickHandler}
|
||||
aria-label={localize('com_ui_manage')}
|
||||
>
|
||||
{localize('com_ui_manage')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
export { default as BackToChat } from './BackToChat';
|
||||
export { default as ManagePrompts } from './ManagePrompts';
|
||||
export { default as AdminSettings } from './AdminSettings';
|
||||
export { default as AdvancedSwitch } from './AdvancedSwitch';
|
||||
export { default as AlwaysMakeProd } from './AlwaysMakeProd';
|
||||
export { default as AutoSendPrompt } from './AutoSendPrompt';
|
||||
export { default as CreatePromptButton } from './CreatePromptButton';
|
||||
|
|
|
|||
|
|
@ -31,13 +31,14 @@ const DeleteConfirmDialog = ({
|
|||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="size-9"
|
||||
aria-label={localize('com_ui_delete')}
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" aria-hidden="true" />
|
||||
<Trash2 className="size-5" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -61,10 +61,11 @@ const SharePrompt = React.memo(
|
|||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-9 border-border-medium"
|
||||
aria-label={localize('com_ui_share')}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Share2Icon className="size-4" aria-hidden="true" />
|
||||
<Share2Icon className="size-5" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const PromptDetails = ({ group, showActions = true, onUsePrompt }: PromptDetails
|
|||
<PromptVariables promptText={mainText} />
|
||||
|
||||
{group.command && (
|
||||
<div className="flex items-center gap-2 rounded-xl border border-border-light bg-surface-secondary p-3">
|
||||
<div className="flex items-center gap-2 rounded-xl border border-border-medium bg-transparent p-3">
|
||||
<SquareSlash className="h-4 w-4 text-text-secondary" aria-hidden="true" />
|
||||
<span className="font-mono text-sm text-text-primary">/{group.command}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ const PromptTextCard = ({ mainText }: PromptTextCardProps) => {
|
|||
}, [mainText, showToast, localize, isCopied]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col rounded-xl border border-border-light bg-transparent shadow-md">
|
||||
<header className="flex shrink-0 items-center justify-between border-b border-border-light p-3">
|
||||
<div className="flex h-full flex-col rounded-xl border border-border-medium bg-transparent">
|
||||
<header className="flex shrink-0 items-center justify-between border-b border-border-medium p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5 text-text-secondary" aria-hidden="true" />
|
||||
<h3 className="text-base font-semibold text-text-primary">
|
||||
|
|
@ -74,9 +74,9 @@ const PromptTextCard = ({ mainText }: PromptTextCardProps) => {
|
|||
aria-live="polite"
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="size-4" aria-hidden="true" />
|
||||
<Check className="size-4 text-text-secondary" aria-hidden="true" />
|
||||
) : (
|
||||
<Copy className="size-4" aria-hidden="true" />
|
||||
<Copy className="size-4 text-text-secondary" aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const DropdownVariableCard = ({ parsed }: { parsed: ParsedVariable }) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="bg-surface-secondary/50 rounded-lg border border-border-light p-2.5 hover:bg-surface-secondary"
|
||||
className="bg-surface-secondary/50 rounded-lg border border-border-medium p-2.5 hover:bg-surface-secondary"
|
||||
role="listitem"
|
||||
aria-label={localize('com_ui_variable_with_options', {
|
||||
name: parsed.name,
|
||||
|
|
@ -61,7 +61,7 @@ const DropdownVariableCard = ({ parsed }: { parsed: ParsedVariable }) => {
|
|||
{parsed.options.map((option, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="rounded-md border border-border-light bg-surface-primary px-2 py-0.5 text-xs text-text-secondary transition-colors hover:bg-surface-secondary"
|
||||
className="rounded-md border border-border-medium bg-transparent px-2 py-0.5 text-xs text-text-secondary transition-colors hover:bg-surface-secondary"
|
||||
role="listitem"
|
||||
>
|
||||
{option}
|
||||
|
|
@ -82,7 +82,7 @@ const SpecialVariableChip = ({ parsed }: { parsed: ParsedVariable }) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="group flex items-start gap-2 rounded-lg border border-border-light bg-transparent p-2 hover:bg-surface-secondary"
|
||||
className="group flex items-start gap-2 rounded-lg border border-border-medium bg-transparent p-2 hover:bg-surface-secondary"
|
||||
role="listitem"
|
||||
aria-label={displayLabel}
|
||||
>
|
||||
|
|
@ -99,7 +99,7 @@ const SpecialVariableChip = ({ parsed }: { parsed: ParsedVariable }) => {
|
|||
|
||||
const SimpleVariableChip = ({ parsed }: { parsed: ParsedVariable }) => (
|
||||
<span
|
||||
className="bg-surface-secondary/50 inline-flex items-center gap-1.5 rounded-lg border border-border-light px-2.5 py-1.5 text-xs font-medium text-text-primary hover:bg-surface-tertiary"
|
||||
className="bg-surface-secondary/50 inline-flex items-center gap-1.5 rounded-lg border border-border-medium px-2.5 py-1.5 text-xs font-medium text-text-primary hover:bg-surface-tertiary"
|
||||
role="listitem"
|
||||
>
|
||||
<Variable className="size-3 text-text-secondary" aria-hidden="true" />
|
||||
|
|
@ -138,8 +138,8 @@ const PromptVariables = ({ promptText }: { promptText: string }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border border-border-light">
|
||||
<header className="flex items-center justify-between border-b border-border-light p-3">
|
||||
<div className="overflow-hidden rounded-xl border border-border-medium">
|
||||
<header className="flex items-center justify-between border-b border-border-medium p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Variable className="size-4 text-text-secondary" aria-hidden="true" />
|
||||
<h4 className="text-sm font-semibold text-text-primary">
|
||||
|
|
|
|||
|
|
@ -52,9 +52,9 @@ const getTimelineConnectorClasses = (isSelected: boolean, isProduction: boolean)
|
|||
return 'border-green-500 bg-green-500 text-white';
|
||||
}
|
||||
if (isProduction) {
|
||||
return 'border-green-400 bg-surface-primary text-green-500';
|
||||
return 'border-green-400 bg-transparent text-green-500';
|
||||
}
|
||||
return 'border-border-medium bg-surface-primary text-text-secondary';
|
||||
return 'border-border-medium bg-transparent text-text-secondary';
|
||||
};
|
||||
|
||||
const VersionCard = ({
|
||||
|
|
@ -107,7 +107,7 @@ const VersionCard = ({
|
|||
'group mb-2 ml-2 flex flex-1 flex-col rounded-lg border p-3 text-left',
|
||||
isSelected
|
||||
? 'border-green-500/50 bg-green-50/50 dark:bg-green-950/20'
|
||||
: 'border-border-light bg-surface-primary hover:border-border-medium hover:bg-surface-hover',
|
||||
: 'border-border-medium bg-transparent hover:border-border-heavy hover:bg-surface-hover',
|
||||
)}
|
||||
onClick={onClick}
|
||||
aria-label={localize('com_ui_version_var', { 0: `${versionNumber}` })}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
return (
|
||||
<div className="flex max-h-[85vh] flex-col sm:max-h-[85vh]">
|
||||
<h2 className="sr-only">{localize('com_ui_control_bar')}</h2>
|
||||
<header className="flex items-center justify-between rounded-t-xl border border-border-light bg-transparent p-2">
|
||||
<header className="flex items-center justify-between rounded-t-xl border border-border-medium bg-transparent px-2 py-1.5">
|
||||
<div className="ml-1 flex items-center gap-2">
|
||||
<FileText className="size-4 text-text-secondary" aria-hidden="true" />
|
||||
<h3 className="text-sm font-semibold text-text-primary">
|
||||
|
|
@ -73,10 +73,8 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
</header>
|
||||
<div
|
||||
className={cn(
|
||||
'relative w-full flex-1 overflow-auto rounded-b-xl border border-t-0 border-border-light p-3 text-left transition-all duration-200 sm:p-4',
|
||||
isEditing
|
||||
? 'bg-surface-primary'
|
||||
: 'cursor-pointer bg-surface-primary hover:bg-surface-secondary',
|
||||
'relative w-full flex-1 overflow-auto rounded-b-xl border border-t-0 border-border-medium p-3 text-left transition-all duration-200 sm:p-4',
|
||||
isEditing ? '' : 'cursor-pointer hover:bg-surface-tertiary',
|
||||
)}
|
||||
>
|
||||
{!isEditing && (
|
||||
|
|
@ -134,10 +132,10 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
{field.value}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
<div className="bg-surface-secondary/0 group-hover/preview:bg-surface-secondary/50 pointer-events-none absolute inset-0 flex items-center justify-center opacity-0 transition-all duration-200 group-hover/preview:opacity-100">
|
||||
<div className="flex items-center gap-2 rounded-lg bg-surface-primary px-3 py-1.5 shadow-md">
|
||||
<div className="pointer-events-none sticky bottom-1/2 z-10 flex translate-y-1/2 items-center justify-center opacity-0 transition-all duration-200 group-hover/preview:opacity-100">
|
||||
<div className="flex items-center gap-2 rounded-lg border border-border-light bg-surface-primary px-3 py-1.5 shadow-md">
|
||||
<EditIcon className="size-4 text-text-secondary" aria-hidden="true" />
|
||||
<span className="text-sm font-medium text-text-secondary">
|
||||
<span className="text-sm font-medium text-text-primary">
|
||||
{localize('com_ui_click_to_edit')}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
|||
const { t } = useTranslation();
|
||||
const formContext = useFormContext();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { hasAccess } = usePromptGroupsContext();
|
||||
const { hasAccess } = usePromptGroupsContext() ?? {};
|
||||
const { categories, emptyCategory } = useCategories({ hasAccess });
|
||||
|
||||
const control = formContext?.control;
|
||||
|
|
@ -74,7 +74,7 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
|||
const trigger = (
|
||||
<Ariakit.MenuButton
|
||||
className={cn(
|
||||
'focus:ring-offset-ring-offset relative inline-flex items-center justify-between rounded-xl border border-input bg-background px-3 py-2 text-sm text-text-primary transition-all duration-200 ease-in-out hover:bg-accent hover:text-accent-foreground focus:ring-ring-primary',
|
||||
'focus:ring-offset-ring-offset relative inline-flex h-9 items-center justify-between rounded-xl border border-border-medium bg-transparent px-3 text-sm text-text-primary transition-all duration-200 ease-in-out hover:bg-accent hover:text-accent-foreground focus:ring-ring-primary',
|
||||
'gap-2 sm:w-fit',
|
||||
className,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const Command = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border-light shadow-md">
|
||||
<div className="rounded-xl border border-border-medium">
|
||||
<label
|
||||
htmlFor="prompt-command"
|
||||
className="block px-4 pt-2 text-sm text-text-secondary md:hidden"
|
||||
|
|
@ -67,7 +67,7 @@ const Command = ({
|
|||
/>
|
||||
<label
|
||||
htmlFor="prompt-command"
|
||||
className="pointer-events-none absolute left-0 top-0.5 hidden max-w-[calc(100%-3.5rem)] origin-[0] translate-y-2 scale-100 rounded bg-surface-primary px-1 text-sm text-text-secondary transition-transform duration-200 peer-placeholder-shown:translate-y-2 peer-placeholder-shown:scale-100 peer-focus:-translate-y-3 peer-focus:scale-75 peer-focus:text-text-primary peer-[:not(:placeholder-shown)]:-translate-y-3 peer-[:not(:placeholder-shown)]:scale-75 md:block"
|
||||
className="pointer-events-none absolute left-0 top-0.5 hidden max-w-[calc(100%-3.5rem)] origin-[0] translate-y-2 scale-100 rounded bg-presentation px-1 text-sm text-text-secondary transition-transform duration-200 peer-placeholder-shown:translate-y-2 peer-placeholder-shown:scale-100 peer-focus:-translate-y-3 peer-focus:scale-75 peer-focus:text-text-primary peer-[:not(:placeholder-shown)]:-translate-y-3 peer-[:not(:placeholder-shown)]:scale-75 md:block"
|
||||
>
|
||||
{localize('com_ui_command_placeholder')}
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const Description = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border-light shadow-md">
|
||||
<div className="rounded-xl border border-border-medium">
|
||||
<label
|
||||
htmlFor="prompt-description"
|
||||
className="block px-4 pt-2 text-sm text-text-secondary md:hidden"
|
||||
|
|
@ -64,7 +64,7 @@ const Description = ({
|
|||
/>
|
||||
<label
|
||||
htmlFor="prompt-description"
|
||||
className="pointer-events-none absolute left-0 top-0.5 hidden max-w-[calc(100%-3.5rem)] origin-[0] translate-y-2 scale-100 rounded bg-surface-primary px-1 text-sm text-text-secondary transition-transform duration-200 peer-placeholder-shown:translate-y-2 peer-placeholder-shown:scale-100 peer-focus:-translate-y-3 peer-focus:scale-75 peer-focus:text-text-primary peer-[:not(:placeholder-shown)]:-translate-y-3 peer-[:not(:placeholder-shown)]:scale-75 md:block"
|
||||
className="pointer-events-none absolute left-0 top-0.5 hidden max-w-[calc(100%-3.5rem)] origin-[0] translate-y-2 scale-100 rounded bg-presentation px-1 text-sm text-text-secondary transition-transform duration-200 peer-placeholder-shown:translate-y-2 peer-placeholder-shown:scale-100 peer-focus:-translate-y-3 peer-focus:scale-75 peer-focus:text-text-primary peer-[:not(:placeholder-shown)]:-translate-y-3 peer-[:not(:placeholder-shown)]:scale-75 md:block"
|
||||
>
|
||||
{localize('com_ui_description_placeholder')}
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -1,70 +1,67 @@
|
|||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { Check, X, Pencil } from 'lucide-react';
|
||||
import { Button, Input, Spinner, TooltipAnchor } from '@librechat/client';
|
||||
import { Pencil, Check, Loader2, X } from 'lucide-react';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
type Props = {
|
||||
name?: string;
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
onSave: (newName: string) => void;
|
||||
};
|
||||
|
||||
const PromptName: React.FC<Props> = ({ name, isLoading = false, onSave }) => {
|
||||
type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
|
||||
|
||||
const PromptName: React.FC<Props> = ({ name, isLoading = false, isError = false, onSave }) => {
|
||||
const localize = useLocalize();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const wasLoadingRef = useRef(false);
|
||||
const savedTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
/** Prevents duplicate saves when Enter/Escape already called commitName before blur fires */
|
||||
const skipBlurRef = useRef(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [newName, setNewName] = useState(name);
|
||||
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle');
|
||||
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNewName(e.target.value);
|
||||
}, []);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
setIsEditing(false);
|
||||
setNewName(name);
|
||||
}, [name, isLoading]);
|
||||
|
||||
const saveName = useCallback(() => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
const commitName = useCallback(() => {
|
||||
const savedName = newName?.trim();
|
||||
if (savedName && savedName !== name) {
|
||||
setSaveStatus('saving');
|
||||
onSave(savedName);
|
||||
} else {
|
||||
setNewName(name);
|
||||
setIsEditing(false);
|
||||
}
|
||||
}, [newName, name, onSave, isLoading]);
|
||||
setIsEditing(false);
|
||||
}, [newName, name, onSave]);
|
||||
|
||||
const saveName = useCallback(() => {
|
||||
if (skipBlurRef.current) {
|
||||
skipBlurRef.current = false;
|
||||
return;
|
||||
}
|
||||
commitName();
|
||||
}, [commitName]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleCancel();
|
||||
e.preventDefault();
|
||||
skipBlurRef.current = true;
|
||||
setNewName(name);
|
||||
setIsEditing(false);
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
saveName();
|
||||
skipBlurRef.current = true;
|
||||
commitName();
|
||||
}
|
||||
},
|
||||
[handleCancel, saveName],
|
||||
[name, commitName],
|
||||
);
|
||||
|
||||
const handleTitleClick = useCallback(() => {
|
||||
setIsEditing(true);
|
||||
}, []);
|
||||
|
||||
const handleTitleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setIsEditing(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
|
|
@ -72,90 +69,89 @@ const PromptName: React.FC<Props> = ({ name, isLoading = false, onSave }) => {
|
|||
}
|
||||
}, [isEditing]);
|
||||
|
||||
// Track loading state for detecting save completion
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
setSaveStatus('saving');
|
||||
} else if (wasLoadingRef.current && !isLoading) {
|
||||
setSaveStatus(isError ? 'error' : 'saved');
|
||||
if (isError) {
|
||||
setNewName(name);
|
||||
}
|
||||
if (savedTimerRef.current) {
|
||||
clearTimeout(savedTimerRef.current);
|
||||
}
|
||||
savedTimerRef.current = setTimeout(() => setSaveStatus('idle'), 2000);
|
||||
}
|
||||
wasLoadingRef.current = isLoading;
|
||||
}, [isLoading]);
|
||||
}, [isLoading, isError, name]);
|
||||
|
||||
// Close editing when name updates after save (loading finished)
|
||||
useEffect(() => {
|
||||
setNewName(name);
|
||||
if (wasLoadingRef.current) {
|
||||
setIsEditing(false);
|
||||
wasLoadingRef.current = false;
|
||||
}
|
||||
}, [name]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (savedTimerRef.current) {
|
||||
clearTimeout(savedTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
<div className="group/title relative mr-2 flex h-8 min-w-0 flex-1 items-center">
|
||||
{isEditing ? (
|
||||
<div className="mr-3 flex min-w-0 flex-1 items-center gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={newName ?? ''}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
ref={inputRef}
|
||||
disabled={isLoading}
|
||||
className="h-10 min-w-0 flex-1 rounded-lg border border-border-medium bg-surface-primary px-3 text-xl font-semibold text-text-primary transition-colors focus:border-border-heavy disabled:opacity-60 sm:text-2xl"
|
||||
aria-label={localize('com_ui_name')}
|
||||
/>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<TooltipAnchor
|
||||
description={isLoading ? localize('com_ui_loading') : localize('com_ui_save')}
|
||||
side="bottom"
|
||||
render={
|
||||
<Button
|
||||
type="button"
|
||||
onClick={saveName}
|
||||
variant="submit"
|
||||
size="icon"
|
||||
disabled={isLoading}
|
||||
aria-label={isLoading ? localize('com_ui_loading') : localize('com_ui_save')}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Spinner size={16} className="text-white" />
|
||||
) : (
|
||||
<Check className="size-4" aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_cancel')}
|
||||
side="bottom"
|
||||
render={
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={isLoading}
|
||||
aria-label={localize('com_ui_cancel')}
|
||||
>
|
||||
<X className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={newName ?? ''}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={saveName}
|
||||
disabled={isLoading}
|
||||
className="h-8 min-w-0 flex-1 rounded-md border border-transparent bg-transparent pl-2 pr-0 text-base font-semibold text-text-primary outline-none focus:border-border-medium focus:outline-none disabled:opacity-60"
|
||||
aria-label={localize('com_ui_name')}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTitleClick}
|
||||
onKeyDown={handleTitleKeyDown}
|
||||
className="group mr-3 flex min-w-0 flex-1 cursor-pointer items-center gap-2 rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
aria-label={localize('com_ui_edit') + ' ' + localize('com_ui_name')}
|
||||
onClick={() => {
|
||||
if (!isLoading && saveStatus !== 'saving') {
|
||||
setIsEditing(true);
|
||||
}
|
||||
}}
|
||||
className="h-8 min-w-0 flex-1 cursor-text truncate pl-2 text-left text-base font-semibold text-text-primary transition-colors hover:text-text-secondary focus:outline-none"
|
||||
title={newName}
|
||||
aria-label={localize('com_ui_edit') + ': ' + (newName ?? '')}
|
||||
>
|
||||
<span className="block truncate text-xl font-semibold text-text-primary sm:text-2xl">
|
||||
{newName}
|
||||
</span>
|
||||
<Pencil
|
||||
className="size-4 shrink-0 text-text-tertiary opacity-0 transition-opacity group-hover:opacity-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{newName}
|
||||
</button>
|
||||
)}
|
||||
<div className="ml-1.5 flex shrink-0 items-center justify-center">
|
||||
{saveStatus === 'saving' && (
|
||||
<Loader2
|
||||
className="size-4 animate-spin text-text-secondary"
|
||||
aria-label={localize('com_ui_saving')}
|
||||
/>
|
||||
)}
|
||||
{saveStatus === 'saved' && (
|
||||
<Check
|
||||
className="size-4 text-green-500 transition-opacity duration-300"
|
||||
aria-label={localize('com_ui_saved')}
|
||||
/>
|
||||
)}
|
||||
{saveStatus === 'error' && (
|
||||
<X
|
||||
className="size-4 text-red-500 transition-opacity duration-300"
|
||||
aria-label={localize('com_ui_error')}
|
||||
/>
|
||||
)}
|
||||
{saveStatus === 'idle' && !isEditing && (
|
||||
<Pencil
|
||||
className="size-3.5 text-text-secondary opacity-0 transition-opacity group-hover/title:opacity-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
|
|||
import { Button, TextareaAutosize, Input } from '@librechat/client';
|
||||
import { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||
import { LocalStorageKeys, PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import OpenSidebar from '~/components/Chat/Menus/OpenSidebar';
|
||||
import CategorySelector from '../fields/CategorySelector';
|
||||
import VariablesDropdown from '../editor/VariablesDropdown';
|
||||
import PromptVariables from '../display/PromptVariables';
|
||||
|
|
@ -34,12 +35,14 @@ const defaultPrompt: CreateFormValues = {
|
|||
|
||||
const CreatePromptForm = ({
|
||||
defaultValues = defaultPrompt,
|
||||
onSuccess,
|
||||
}: {
|
||||
defaultValues?: CreateFormValues;
|
||||
onSuccess?: (groupId: string) => void;
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
const navigate = useNavigate();
|
||||
const { hasAccess: hasUseAccess } = usePromptGroupsContext();
|
||||
const { hasAccess: hasUseAccess } = usePromptGroupsContext() ?? {};
|
||||
const hasCreateAccess = useHasAccess({
|
||||
permissionType: PermissionTypes.PROMPTS,
|
||||
permission: Permissions.CREATE,
|
||||
|
|
@ -48,7 +51,7 @@ const CreatePromptForm = ({
|
|||
|
||||
useEffect(() => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
if (!hasAccess) {
|
||||
if (!hasAccess && !onSuccess) {
|
||||
timeoutId = setTimeout(() => {
|
||||
navigate('/c/new');
|
||||
}, 1000);
|
||||
|
|
@ -56,7 +59,7 @@ const CreatePromptForm = ({
|
|||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [hasAccess, navigate]);
|
||||
}, [hasAccess, navigate, onSuccess]);
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: {
|
||||
|
|
@ -74,7 +77,12 @@ const CreatePromptForm = ({
|
|||
|
||||
const createPromptMutation = useCreatePrompt({
|
||||
onSuccess: (response) => {
|
||||
navigate(`/d/prompts/${response.prompt.groupId}`, { replace: true });
|
||||
const groupId = response.prompt.groupId;
|
||||
if (onSuccess && groupId) {
|
||||
onSuccess(groupId);
|
||||
} else {
|
||||
navigate(`/prompts/${groupId}`, { replace: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -106,6 +114,10 @@ const CreatePromptForm = ({
|
|||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full px-4 py-2">
|
||||
<h1 className="sr-only">{localize('com_ui_create_prompt_page')}</h1>
|
||||
<div className="mb-2 flex items-center justify-between gap-2 sm:hidden">
|
||||
<OpenSidebar />
|
||||
<CategorySelector />
|
||||
</div>
|
||||
<div className="mb-1 flex flex-col items-center justify-between font-bold sm:text-xl md:mb-0 md:text-2xl">
|
||||
<div className="flex w-full flex-col items-center justify-between sm:flex-row">
|
||||
<Controller
|
||||
|
|
@ -118,7 +130,7 @@ const CreatePromptForm = ({
|
|||
{...field}
|
||||
id="prompt-name"
|
||||
type="text"
|
||||
className="peer mr-2 w-full border border-border-light p-2 text-2xl text-text-primary"
|
||||
className="peer mr-2 w-full border border-border-medium p-2 text-2xl text-text-primary"
|
||||
placeholder=" "
|
||||
tabIndex={0}
|
||||
aria-label={localize('com_ui_prompt_name')}
|
||||
|
|
@ -126,7 +138,7 @@ const CreatePromptForm = ({
|
|||
/>
|
||||
<label
|
||||
htmlFor="prompt-name"
|
||||
className="pointer-events-none absolute -top-1 left-3 origin-[0] translate-y-3 scale-100 rounded bg-surface-primary px-1 text-base text-text-secondary transition-transform duration-200 peer-placeholder-shown:translate-y-3 peer-placeholder-shown:scale-100 peer-focus:-translate-y-2 peer-focus:scale-75 peer-focus:text-text-primary peer-[:not(:placeholder-shown)]:-translate-y-2 peer-[:not(:placeholder-shown)]:scale-75"
|
||||
className="pointer-events-none absolute -top-1 left-3 origin-[0] translate-y-3 scale-100 rounded bg-presentation px-1 text-base text-text-secondary transition-transform duration-200 peer-placeholder-shown:translate-y-3 peer-placeholder-shown:scale-100 peer-focus:-translate-y-2 peer-focus:scale-75 peer-focus:text-text-primary peer-[:not(:placeholder-shown)]:-translate-y-2 peer-[:not(:placeholder-shown)]:scale-75"
|
||||
>
|
||||
{localize('com_ui_prompt_name')}*
|
||||
</label>
|
||||
|
|
@ -141,12 +153,14 @@ const CreatePromptForm = ({
|
|||
</div>
|
||||
)}
|
||||
/>
|
||||
<CategorySelector />
|
||||
<div className="hidden sm:block">
|
||||
<CategorySelector />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-4 md:mt-[1.075rem]">
|
||||
<div className="flex flex-col">
|
||||
<header className="flex items-center justify-between rounded-t-xl border border-border-light bg-transparent p-2">
|
||||
<header className="flex items-center justify-between rounded-t-xl border border-border-medium bg-transparent p-2">
|
||||
<div className="ml-1 flex items-center gap-2">
|
||||
<FileText className="size-4 text-text-secondary" aria-hidden="true" />
|
||||
<h2 className="text-sm font-semibold text-text-primary">
|
||||
|
|
@ -157,7 +171,7 @@ const CreatePromptForm = ({
|
|||
<VariablesDropdown fieldName="prompt" />
|
||||
</div>
|
||||
</header>
|
||||
<div className="min-h-32 rounded-b-xl border border-t-0 border-border-light p-3 sm:p-4">
|
||||
<div className="min-h-32 rounded-b-xl border border-t-0 border-border-medium p-3 sm:p-4">
|
||||
<Controller
|
||||
name="prompt"
|
||||
control={control}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Menu, Rocket } from 'lucide-react';
|
||||
import { Menu, Rocket, X } from 'lucide-react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { Button, Skeleton, useToastContext } from '@librechat/client';
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
useMakePromptProduction,
|
||||
} from '~/data-provider';
|
||||
import { useResourcePermissions, useHasAccess, useLocalize, useFocusTrap } from '~/hooks';
|
||||
import OpenSidebar from '~/components/Chat/Menus/OpenSidebar';
|
||||
import CategorySelector from '../fields/CategorySelector';
|
||||
import PromptVariables from '../display/PromptVariables';
|
||||
import PromptVersions from '../display/PromptVersions';
|
||||
|
|
@ -65,18 +66,15 @@ const VersionsPanel = React.memo(
|
|||
const isProductionVersion = selectedPrompt?._id === group?.productionId;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-full w-full flex-col overflow-hidden bg-surface-primary"
|
||||
style={{ maxHeight: 'calc(100vh - 100px)' }}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
{canEdit && (
|
||||
<div className="shrink-0 border-b border-border-light px-4 py-3">
|
||||
<div className="shrink-0 px-4 py-2">
|
||||
<Button
|
||||
variant="submit"
|
||||
size="sm"
|
||||
aria-label={localize('com_ui_make_production')}
|
||||
className={cn(
|
||||
'w-full gap-2 transition-all duration-200',
|
||||
'w-full gap-1.5 transition-all duration-200',
|
||||
isProductionVersion &&
|
||||
'border border-green-500/30 bg-green-50 text-green-700 hover:bg-green-100 dark:bg-green-950/30 dark:text-green-400 dark:hover:bg-green-950/50',
|
||||
)}
|
||||
|
|
@ -106,7 +104,7 @@ const VersionsPanel = React.memo(
|
|||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-3">
|
||||
<div className="flex-1 overflow-y-auto px-4 py-2">
|
||||
{isLoadingPrompts &&
|
||||
Array.from({ length: 6 }).map((_, index: number) => (
|
||||
<div key={index} className="my-2">
|
||||
|
|
@ -115,7 +113,7 @@ const VersionsPanel = React.memo(
|
|||
))}
|
||||
{!isLoadingPrompts && prompts.length > 0 && (
|
||||
<>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-sm font-medium text-text-secondary">
|
||||
{localize('com_ui_versions')}
|
||||
</h2>
|
||||
|
|
@ -180,13 +178,13 @@ const HeaderActions = React.memo(
|
|||
|
||||
HeaderActions.displayName = 'HeaderActions';
|
||||
|
||||
const PromptForm = () => {
|
||||
const PromptForm = ({ promptId: promptIdProp }: { promptId?: string }) => {
|
||||
const params = useParams();
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const { hasAccess } = usePromptGroupsContext();
|
||||
const { hasAccess, groupsQuery } = usePromptGroupsContext() ?? {};
|
||||
const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd);
|
||||
const promptId = params.promptId || '';
|
||||
const promptId = promptIdProp || params.promptId || '';
|
||||
|
||||
const editorMode = useRecoilValue(store.promptsEditorMode);
|
||||
const [selectionIndex, setSelectionIndex] = useState<number>(0);
|
||||
|
|
@ -197,7 +195,6 @@ const PromptForm = () => {
|
|||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [initialLoad, setInitialLoad] = useState(true);
|
||||
const [showSidePanel, setShowSidePanel] = useState(false);
|
||||
const sidePanelWidth = '320px';
|
||||
|
||||
// Reset selection when navigating to a different prompt group
|
||||
useEffect(() => {
|
||||
|
|
@ -239,8 +236,6 @@ const PromptForm = () => {
|
|||
|
||||
const selectedPromptId = useMemo(() => selectedPrompt?._id, [selectedPrompt?._id]);
|
||||
|
||||
const { groupsQuery } = usePromptGroupsContext();
|
||||
|
||||
const updateGroupMutation = useUpdatePromptGroup({
|
||||
onError: () => {
|
||||
showToast({
|
||||
|
|
@ -335,7 +330,7 @@ const PromptForm = () => {
|
|||
|
||||
useEffect(() => {
|
||||
handleLoadingComplete();
|
||||
}, [params.promptId, editorMode, group?.productionId, prompts, handleLoadingComplete]);
|
||||
}, [promptId, editorMode, group?.productionId, prompts, handleLoadingComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue('prompt', selectedPrompt ? selectedPrompt.prompt : '', { shouldDirty: false });
|
||||
|
|
@ -435,11 +430,8 @@ const PromptForm = () => {
|
|||
}
|
||||
|
||||
// Show read-only view if user doesn't have edit permission
|
||||
if (!canEdit && !permissionsLoading && groupsQuery.data) {
|
||||
const fetchedPrompt = findPromptGroup(
|
||||
groupsQuery.data,
|
||||
(group) => group._id === params.promptId,
|
||||
);
|
||||
if (!canEdit && !permissionsLoading && groupsQuery?.data) {
|
||||
const fetchedPrompt = findPromptGroup(groupsQuery?.data, (group) => group._id === promptId);
|
||||
if (!fetchedPrompt && !canView) {
|
||||
return <NoPromptGroup />;
|
||||
}
|
||||
|
|
@ -457,22 +449,42 @@ const PromptForm = () => {
|
|||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form className="mt-4 flex w-full" onSubmit={handleSubmit((data) => onSave(data.prompt))}>
|
||||
<form className="flex w-full" onSubmit={handleSubmit((data) => onSave(data.prompt))}>
|
||||
<h1 className="sr-only">{localize('com_ui_edit_prompt_page')}</h1>
|
||||
<div className="relative w-full">
|
||||
<div className="h-full w-full">
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<div
|
||||
className="h-full w-full"
|
||||
style={{
|
||||
transform: showSidePanel ? 'translateX(max(-85vw, -380px))' : 'translateX(0)',
|
||||
transition: 'transform 300ms cubic-bezier(0.2, 0, 0, 1)',
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
<div className="flex-1 overflow-hidden px-4">
|
||||
{/* Mobile Actions Row */}
|
||||
{!isLoadingGroup && group && (
|
||||
<div className="mb-3 mt-2 flex items-center justify-between gap-2 sm:hidden">
|
||||
<OpenSidebar />
|
||||
<HeaderActions
|
||||
group={group}
|
||||
canEdit={canEdit}
|
||||
canDelete={canDelete}
|
||||
selectedPromptId={selectedPromptId}
|
||||
onCategoryChange={handleCategoryChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Header: Title + Actions */}
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="mb-3 mt-2 flex items-center justify-between gap-2">
|
||||
{isLoadingGroup ? (
|
||||
<Skeleton className="h-10 w-48 font-bold sm:text-xl md:h-12 md:text-2xl" />
|
||||
<Skeleton className="h-9 w-48" />
|
||||
) : (
|
||||
<>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<PromptName
|
||||
name={groupName}
|
||||
isLoading={updateGroupMutation.isLoading}
|
||||
isError={updateGroupMutation.isError}
|
||||
onSave={(value) => {
|
||||
if (!canEdit || !group._id) {
|
||||
return;
|
||||
|
|
@ -493,8 +505,8 @@ const PromptForm = () => {
|
|||
onClick={() => setShowSidePanel(true)}
|
||||
aria-label={localize('com_ui_versions')}
|
||||
>
|
||||
<Menu className="mr-1.5 size-4" aria-hidden="true" />
|
||||
<span>{localize('com_ui_versions')}</span>
|
||||
<Menu className="size-4 sm:mr-1.5" aria-hidden="true" />
|
||||
<span className="hidden sm:inline">{localize('com_ui_versions')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -511,24 +523,11 @@ const PromptForm = () => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Actions Row */}
|
||||
{!isLoadingGroup && group && (
|
||||
<div className="mb-4 sm:hidden">
|
||||
<HeaderActions
|
||||
group={group}
|
||||
canEdit={canEdit}
|
||||
canDelete={canDelete}
|
||||
selectedPromptId={selectedPromptId}
|
||||
onCategoryChange={handleCategoryChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Editor Content */}
|
||||
{isLoadingPrompts ? (
|
||||
<Skeleton className="h-96" aria-live="polite" />
|
||||
) : (
|
||||
<div className="mb-2 flex h-full flex-col gap-4">
|
||||
<div className="mb-2 flex h-full flex-col gap-3">
|
||||
<PromptEditor
|
||||
name="prompt"
|
||||
isEditing={isEditing}
|
||||
|
|
@ -551,7 +550,7 @@ const PromptForm = () => {
|
|||
|
||||
{/* Versions Sidebar - Advanced Mode Only */}
|
||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||
<div className="hidden w-72 shrink-0 border-l border-border-light lg:block xl:w-80">
|
||||
<div className="hidden w-72 shrink-0 border-l border-border-medium lg:block xl:w-80">
|
||||
<VersionsPanel
|
||||
group={group}
|
||||
prompts={prompts}
|
||||
|
|
@ -567,56 +566,63 @@ const PromptForm = () => {
|
|||
</div>
|
||||
|
||||
{/* Mobile Overlay */}
|
||||
{showSidePanel && (
|
||||
<div
|
||||
aria-hidden={!showSidePanel}
|
||||
className={cn(
|
||||
'fixed inset-0 z-[100] bg-black/20 lg:hidden',
|
||||
showSidePanel ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
style={{ transition: 'opacity 300ms cubic-bezier(0.2, 0, 0, 1)' }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-0 z-40 cursor-default bg-black/20"
|
||||
style={{ transition: 'opacity 0.3s ease-in-out' }}
|
||||
className="h-full w-full cursor-default"
|
||||
onClick={() => setShowSidePanel(false)}
|
||||
aria-label={localize('com_ui_close_menu')}
|
||||
tabIndex={showSidePanel ? 0 : -1}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Versions Panel */}
|
||||
<div
|
||||
ref={sidePanelRef}
|
||||
className="absolute inset-y-0 right-0 z-50 lg:hidden"
|
||||
className={cn(
|
||||
'fixed right-0 top-0 z-[110] flex h-full flex-col border-l border-border-medium bg-surface-primary-alt shadow-xl lg:hidden',
|
||||
showSidePanel ? 'translate-x-0' : 'translate-x-full',
|
||||
)}
|
||||
style={{
|
||||
width: sidePanelWidth,
|
||||
transform: `translateX(${showSidePanel ? '0' : '100%'})`,
|
||||
transition: 'transform 0.3s ease-in-out',
|
||||
willChange: 'transform',
|
||||
width: 'min(85vw, 380px)',
|
||||
transition: 'transform 300ms cubic-bezier(0.2, 0, 0, 1)',
|
||||
}}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={localize('com_ui_versions')}
|
||||
inert={!showSidePanel ? '' : undefined}
|
||||
>
|
||||
<div className="h-full bg-surface-primary shadow-xl">
|
||||
<div className="flex items-center justify-between border-b border-border-light px-4 py-3">
|
||||
<h2 className="text-lg font-semibold text-text-primary">
|
||||
{localize('com_ui_versions')}
|
||||
</h2>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowSidePanel(false)}
|
||||
aria-label={localize('com_ui_close')}
|
||||
>
|
||||
<span className="sr-only">{localize('com_ui_close')}</span>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
<VersionsPanel
|
||||
group={group}
|
||||
prompts={prompts}
|
||||
selectionIndex={selectionIndex}
|
||||
selectedPrompt={selectedPrompt}
|
||||
isLoadingPrompts={isLoadingPrompts}
|
||||
canEdit={canEdit}
|
||||
setSelectionIndex={setSelectionIndex}
|
||||
/>
|
||||
<div className="flex items-center justify-between px-4 py-2">
|
||||
<h2 className="text-sm font-semibold text-text-primary">
|
||||
{localize('com_ui_versions')}
|
||||
</h2>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowSidePanel(false)}
|
||||
aria-label={localize('com_ui_close')}
|
||||
className="size-8"
|
||||
>
|
||||
<X className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
<VersionsPanel
|
||||
group={group}
|
||||
prompts={prompts}
|
||||
selectionIndex={selectionIndex}
|
||||
selectedPrompt={selectedPrompt}
|
||||
isLoadingPrompts={isLoadingPrompts}
|
||||
canEdit={canEdit}
|
||||
setSelectionIndex={setSelectionIndex}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ const PromptLabelsForm = ({ selectedPrompt }: { selectedPrompt?: TPrompt }) => {
|
|||
onKeyDown={handleKeyDown}
|
||||
aria-label={localize('com_ui_add_labels')}
|
||||
/>
|
||||
<h3 className="rounded-t-lg border border-border-light px-4 text-base font-semibold text-text-primary">
|
||||
<h3 className="rounded-t-lg border border-border-medium px-4 text-base font-semibold text-text-primary">
|
||||
{localize('com_ui_labels')}
|
||||
</h3>
|
||||
<div className="mb-4 flex w-full flex-row flex-wrap rounded-b-lg border border-border-light p-4">
|
||||
<div className="mb-4 flex w-full flex-row flex-wrap rounded-b-lg border border-border-medium p-4">
|
||||
{labels.length ? (
|
||||
labels.map((label, index) => (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export { PromptsView } from './layouts';
|
||||
export { InlinePromptsView } from './layouts';
|
||||
export { CategoryIcon, SkeletonForm } from './utils';
|
||||
export { PromptName, Command, Description, CategorySelector } from './fields';
|
||||
export { PreviewPrompt, DeleteVersion, VariableDialog, SharePrompt } from './dialogs';
|
||||
|
|
@ -11,19 +11,5 @@ export {
|
|||
FilterPrompts,
|
||||
PanelNavigation,
|
||||
} from './sidebar';
|
||||
export {
|
||||
List as PromptGroupsList,
|
||||
DashGroupItem,
|
||||
ChatGroupItem,
|
||||
ListCard,
|
||||
NoPromptGroup,
|
||||
} from './lists';
|
||||
export {
|
||||
CreatePromptButton,
|
||||
AdminSettings,
|
||||
AdvancedSwitch,
|
||||
AlwaysMakeProd,
|
||||
AutoSendPrompt,
|
||||
BackToChat,
|
||||
ManagePrompts,
|
||||
} from './buttons';
|
||||
export { List as PromptGroupsList, ChatGroupItem, ListCard, NoPromptGroup } from './lists';
|
||||
export { CreatePromptButton, AdminSettings, AlwaysMakeProd, AutoSendPrompt } from './buttons';
|
||||
|
|
|
|||
48
client/src/components/Prompts/layouts/InlinePromptsView.tsx
Normal file
48
client/src/components/Prompts/layouts/InlinePromptsView.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useParams, useNavigate, Navigate } from 'react-router-dom';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import EmptyPromptPreview from '../display/EmptyPromptPreview';
|
||||
import CreatePromptForm from '../forms/CreatePromptForm';
|
||||
import { useHasAccess } from '~/hooks';
|
||||
import PromptForm from '../forms/PromptForm';
|
||||
|
||||
export default function InlinePromptsView() {
|
||||
const { promptId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const isNew = promptId === undefined;
|
||||
|
||||
const hasAccess = useHasAccess({
|
||||
permissionType: PermissionTypes.PROMPTS,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
|
||||
const hasCreateAccess = useHasAccess({
|
||||
permissionType: PermissionTypes.PROMPTS,
|
||||
permission: Permissions.CREATE,
|
||||
});
|
||||
|
||||
const handleCreateSuccess = useCallback(
|
||||
(groupId: string) => {
|
||||
navigate(`/prompts/${groupId}`, { replace: true });
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
if (!hasAccess) {
|
||||
return <Navigate to="/c/new" replace />;
|
||||
}
|
||||
|
||||
if (isNew && !hasCreateAccess) {
|
||||
return <EmptyPromptPreview />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto bg-presentation">
|
||||
{isNew ? (
|
||||
<CreatePromptForm onSuccess={handleCreateSuccess} />
|
||||
) : (
|
||||
<PromptForm promptId={promptId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
import { useMemo, useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { Sidebar, useMediaQuery } from '@librechat/client';
|
||||
import { Outlet, useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { PermissionTypes, Permissions, SystemRoles } from 'librechat-data-provider';
|
||||
import { AdvancedSwitch, AdminSettings } from '~/components/Prompts';
|
||||
import { useHasAccess, useLocalize, useAuthContext } from '~/hooks';
|
||||
import DashBreadcrumb from '~/routes/Layouts/DashBreadcrumb';
|
||||
import GroupSidePanel from '../sidebar/GroupSidePanel';
|
||||
import FilterPrompts from '../sidebar/FilterPrompts';
|
||||
import { PromptGroupsProvider } from '~/Providers';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const promptsPathPattern = /prompts\/(?!new(?:\/|$)).*$/;
|
||||
|
||||
export default function PromptsView() {
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
const { user } = useAuthContext();
|
||||
|
||||
const isDetailView = useMemo(() => !!(params.promptId || params['*'] === 'new'), [params]);
|
||||
const isSmallerScreen = useMediaQuery('(max-width: 768px)');
|
||||
const [panelVisible, setPanelVisible] = useState(!isSmallerScreen);
|
||||
const openPanelRef = useRef<HTMLButtonElement>(null);
|
||||
const closePanelRef = useRef<HTMLButtonElement>(null);
|
||||
const isPromptsPath = useMemo(
|
||||
() => promptsPathPattern.test(location.pathname),
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const hasAccess = useHasAccess({
|
||||
permissionType: PermissionTypes.PROMPTS,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
if (!hasAccess) {
|
||||
timeoutId = setTimeout(() => {
|
||||
navigate('/c/new');
|
||||
}, 1000);
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [hasAccess, navigate]);
|
||||
|
||||
const togglePanel = useCallback(() => {
|
||||
setPanelVisible((prev) => {
|
||||
const newValue = !prev;
|
||||
requestAnimationFrame(() => {
|
||||
if (newValue) {
|
||||
closePanelRef?.current?.focus();
|
||||
} else {
|
||||
openPanelRef?.current?.focus();
|
||||
}
|
||||
});
|
||||
return newValue;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSmallerScreen && isDetailView) {
|
||||
setPanelVisible(false);
|
||||
}
|
||||
}, [isSmallerScreen, isDetailView]);
|
||||
|
||||
if (!hasAccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PromptGroupsProvider>
|
||||
<div className="flex h-screen w-full flex-col bg-surface-primary p-0 lg:p-2">
|
||||
{isSmallerScreen && isDetailView ? (
|
||||
<div className="mr-2 mt-2 flex h-10 items-center justify-between">
|
||||
<button
|
||||
ref={openPanelRef}
|
||||
type="button"
|
||||
onClick={togglePanel}
|
||||
className="ml-2 flex h-8 w-8 items-center justify-center rounded-lg border border-border-medium bg-surface-primary text-text-primary transition-all hover:bg-surface-hover"
|
||||
aria-label={localize('com_nav_open_sidebar')}
|
||||
aria-expanded={false}
|
||||
aria-controls="prompts-panel"
|
||||
>
|
||||
<Sidebar className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{isPromptsPath && <AdvancedSwitch />}
|
||||
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<DashBreadcrumb />
|
||||
)}
|
||||
<div className="flex w-full flex-grow flex-row overflow-hidden">
|
||||
{isSmallerScreen && panelVisible && isDetailView && (
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-black/50 transition-opacity"
|
||||
onClick={togglePanel}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={localize('com_nav_toggle_sidebar')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(!isSmallerScreen || !isDetailView || panelVisible) && (
|
||||
<div
|
||||
className={cn(
|
||||
'transition-transform duration-300 ease-in-out',
|
||||
isSmallerScreen && isDetailView
|
||||
? 'fixed left-0 top-0 z-50 h-full w-[320px] bg-surface-primary'
|
||||
: 'flex',
|
||||
)}
|
||||
>
|
||||
<GroupSidePanel
|
||||
closePanelRef={closePanelRef}
|
||||
onClose={isSmallerScreen && isDetailView ? togglePanel : undefined}
|
||||
>
|
||||
<div className="mt-1 flex flex-row items-center justify-between px-2">
|
||||
<FilterPrompts dropdownClassName="z-[100]" />
|
||||
</div>
|
||||
</GroupSidePanel>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'scrollbar-gutter-stable min-w-0 flex-1 overflow-y-auto',
|
||||
isDetailView ? 'block' : 'hidden md:block',
|
||||
)}
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PromptGroupsProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
export { default as PromptsView } from './PromptsView';
|
||||
export { default as InlinePromptsView } from './InlinePromptsView';
|
||||
|
|
|
|||
|
|
@ -1,36 +1,89 @@
|
|||
import { useState, memo, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, TooltipAnchor } from '@librechat/client';
|
||||
import { Eye, Pencil, EarthIcon, User } from 'lucide-react';
|
||||
import { useState, memo, useRef, useCallback, useId, useMemo } from 'react';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Ellipsis, Eye, SquarePen, Trash, EarthIcon, User } from 'lucide-react';
|
||||
import { PermissionBits, ResourceType } from 'librechat-data-provider';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import {
|
||||
Label,
|
||||
Button,
|
||||
Spinner,
|
||||
OGDialog,
|
||||
TooltipAnchor,
|
||||
DropdownPopup,
|
||||
OGDialogTemplate,
|
||||
useToastContext,
|
||||
} from '@librechat/client';
|
||||
import { useLocalize, useAuthContext, useSubmitMessage, useResourcePermissions } from '~/hooks';
|
||||
import { useRecordPromptUsage } from '~/data-provider';
|
||||
import { useRecordPromptUsage, useDeletePromptGroup } from '~/data-provider';
|
||||
import { useLiveAnnouncer } from '~/Providers';
|
||||
import VariableDialog from '../dialogs/VariableDialog';
|
||||
import PreviewPrompt from '../dialogs/PreviewPrompt';
|
||||
import { detectVariables } from '~/utils';
|
||||
import ListCard from './ListCard';
|
||||
import CategoryIcon from '../utils/CategoryIcon';
|
||||
import { detectVariables, cn } from '~/utils';
|
||||
|
||||
function ChatGroupItem({ group }: { group: TPromptGroup }) {
|
||||
const PROMPT_PATH = '/prompts';
|
||||
|
||||
function ChatGroupItem({
|
||||
group,
|
||||
isChatRoute = true,
|
||||
}: {
|
||||
group: TPromptGroup;
|
||||
isChatRoute?: boolean;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const { user } = useAuthContext();
|
||||
const { submitPrompt } = useSubmitMessage();
|
||||
const recordUsage = useRecordPromptUsage();
|
||||
const { announcePolite } = useLiveAnnouncer();
|
||||
|
||||
const { showToast } = useToastContext();
|
||||
const menuId = useId();
|
||||
const isSharedPrompt = group.author !== user?.id && Boolean(group.authorName);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [isPreviewDialogOpen, setPreviewDialogOpen] = useState(false);
|
||||
const [isVariableDialogOpen, setVariableDialogOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
|
||||
const groupIsGlobal = group.isPublic === true;
|
||||
|
||||
// Check permissions for the promptGroup
|
||||
const { hasPermission } = useResourcePermissions(ResourceType.PROMPTGROUP, group._id || '');
|
||||
const canEdit = hasPermission(PermissionBits.EDIT);
|
||||
const canDelete = hasPermission(PermissionBits.DELETE);
|
||||
|
||||
const previewButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const menuButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const deleteGroup = useDeletePromptGroup({
|
||||
onSuccess: () => {
|
||||
setDeleteOpen(false);
|
||||
announcePolite({
|
||||
message: localize('com_ui_prompt_deleted_group', { 0: group.name }),
|
||||
isStatus: true,
|
||||
});
|
||||
if (!isChatRoute && params.promptId === group._id) {
|
||||
navigate(`${PROMPT_PATH}/new`, { replace: true });
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
showToast({ status: 'error', message: localize('com_ui_prompt_delete_error') });
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!group._id) {
|
||||
return;
|
||||
}
|
||||
deleteGroup.mutate({ id: group._id });
|
||||
};
|
||||
|
||||
const onCardClick = useCallback(() => {
|
||||
if (!isChatRoute) {
|
||||
navigate(`${PROMPT_PATH}/${group._id}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const onCardClick = () => {
|
||||
const text = group.productionPrompt?.prompt;
|
||||
if (!text?.trim()) {
|
||||
return;
|
||||
|
|
@ -45,91 +98,133 @@ function ChatGroupItem({ group }: { group: TPromptGroup }) {
|
|||
if (group._id) {
|
||||
recordUsage.mutate(group._id);
|
||||
}
|
||||
};
|
||||
}, [group, submitPrompt, recordUsage, isChatRoute, navigate]);
|
||||
|
||||
const snippet =
|
||||
typeof group.oneliner === 'string' && group.oneliner.length > 0
|
||||
? group.oneliner
|
||||
: (group.productionPrompt?.prompt ?? '');
|
||||
|
||||
const ariaLabel = group.category
|
||||
? localize('com_ui_prompt_group_button', { name: group.name, category: group.category })
|
||||
: localize('com_ui_prompt_group_button_no_category', { name: group.name });
|
||||
|
||||
const dropdownItems = useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
label: localize('com_ui_preview'),
|
||||
onClick: () => setPreviewDialogOpen(true),
|
||||
icon: <Eye className="icon-sm mr-2 text-text-primary" aria-hidden="true" />,
|
||||
},
|
||||
];
|
||||
if (canEdit) {
|
||||
items.push({
|
||||
label: localize('com_ui_edit'),
|
||||
onClick: () => navigate(`${PROMPT_PATH}/${group._id}`),
|
||||
icon: <SquarePen className="icon-sm mr-2 text-text-primary" aria-hidden="true" />,
|
||||
});
|
||||
}
|
||||
if (canDelete) {
|
||||
items.push({
|
||||
label: localize('com_ui_delete'),
|
||||
onClick: () => setDeleteOpen(true),
|
||||
icon: <Trash className="icon-sm mr-2 text-text-primary" aria-hidden="true" />,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}, [localize, canEdit, canDelete, group._id, navigate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 rounded-xl border border-border-light bg-transparent px-1 hover:bg-surface-secondary">
|
||||
<ListCard
|
||||
name={group.name}
|
||||
category={group.category ?? ''}
|
||||
<div
|
||||
className={cn(
|
||||
'group/prompt relative mb-1.5 rounded-xl border border-border-light bg-transparent transition-colors hover:bg-surface-secondary',
|
||||
!isChatRoute && params.promptId === group._id && 'bg-surface-hover',
|
||||
)}
|
||||
>
|
||||
{/* Clickable overlay for card */}
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-0 z-0 rounded-xl focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
|
||||
onClick={onCardClick}
|
||||
snippet={
|
||||
typeof group.oneliner === 'string' && group.oneliner.length > 0
|
||||
? group.oneliner
|
||||
: (group.productionPrompt?.prompt ?? '')
|
||||
}
|
||||
icon={
|
||||
isSharedPrompt || groupIsGlobal ? (
|
||||
<>
|
||||
{isSharedPrompt && (
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_by_author', { 0: group.authorName })}
|
||||
side="top"
|
||||
render={
|
||||
<span
|
||||
tabIndex={0}
|
||||
role="img"
|
||||
aria-label={localize('com_ui_by_author', { 0: group.authorName })}
|
||||
className="flex shrink-0 cursor-default items-center rounded-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
|
||||
>
|
||||
<User className="icon-md text-text-secondary" aria-hidden="true" />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{groupIsGlobal && (
|
||||
<EarthIcon
|
||||
className="icon-md shrink-0 text-green-400"
|
||||
aria-label={localize('com_ui_sr_global_prompt')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_preview')}
|
||||
side="top"
|
||||
render={
|
||||
<Button
|
||||
ref={previewButtonRef}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
aria-label={localize('com_ui_preview')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setPreviewDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Eye className="size-4 text-text-primary" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{canEdit && (
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_edit')}
|
||||
side="top"
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
aria-label={localize('com_ui_edit')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/d/prompts/${group._id}`);
|
||||
}}
|
||||
>
|
||||
<Pencil className="size-4 text-text-primary" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
/>
|
||||
<div className="flex items-start gap-2.5 px-3 py-2.5">
|
||||
<CategoryIcon
|
||||
category={group.category ?? ''}
|
||||
className="mt-0.5 size-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate text-sm font-semibold text-text-primary" title={group.name}>
|
||||
{group.name}
|
||||
</span>
|
||||
{isSharedPrompt && (
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_by_author', { 0: group.authorName })}
|
||||
side="top"
|
||||
render={
|
||||
<span
|
||||
tabIndex={0}
|
||||
role="img"
|
||||
aria-label={localize('com_ui_by_author', { 0: group.authorName })}
|
||||
className="flex shrink-0 items-center"
|
||||
>
|
||||
<User className="size-3.5 text-text-secondary" aria-hidden="true" />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{groupIsGlobal && (
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_sr_global_prompt')}
|
||||
side="top"
|
||||
render={
|
||||
<span
|
||||
tabIndex={0}
|
||||
role="img"
|
||||
aria-label={localize('com_ui_sr_global_prompt')}
|
||||
className="flex shrink-0 items-center"
|
||||
>
|
||||
<EarthIcon className="size-3.5 text-green-400" aria-hidden="true" />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-0.5 line-clamp-2 text-xs leading-relaxed text-text-secondary">
|
||||
{snippet}
|
||||
</p>
|
||||
</div>
|
||||
</ListCard>
|
||||
<div className="relative z-10 shrink-0">
|
||||
<DropdownPopup
|
||||
portal={true}
|
||||
menuId={menuId}
|
||||
focusLoop={true}
|
||||
className="z-[125]"
|
||||
unmountOnHide={true}
|
||||
isOpen={menuOpen}
|
||||
setIsOpen={setMenuOpen}
|
||||
trigger={
|
||||
<Ariakit.MenuButton
|
||||
ref={menuButtonRef}
|
||||
aria-label={localize('com_nav_convo_menu_options')}
|
||||
className={cn(
|
||||
'flex size-7 items-center justify-center rounded-md text-text-secondary transition-opacity hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary',
|
||||
menuOpen
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 focus-visible:opacity-100 group-hover/prompt:opacity-100',
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Ellipsis className="size-4" aria-hidden="true" />
|
||||
</Ariakit.MenuButton>
|
||||
}
|
||||
items={dropdownItems}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PreviewPrompt
|
||||
group={group}
|
||||
|
|
@ -137,15 +232,29 @@ function ChatGroupItem({ group }: { group: TPromptGroup }) {
|
|||
onOpenChange={setPreviewDialogOpen}
|
||||
onCloseAutoFocus={() => {
|
||||
requestAnimationFrame(() => {
|
||||
previewButtonRef.current?.focus({ preventScroll: true });
|
||||
menuButtonRef.current?.focus({ preventScroll: true });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<VariableDialog
|
||||
open={isVariableDialogOpen}
|
||||
onClose={() => setVariableDialogOpen(false)}
|
||||
group={group}
|
||||
/>
|
||||
{isChatRoute && (
|
||||
<VariableDialog
|
||||
open={isVariableDialogOpen}
|
||||
onClose={() => setVariableDialogOpen(false)}
|
||||
group={group}
|
||||
/>
|
||||
)}
|
||||
<OGDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||
<OGDialogTemplate
|
||||
title={localize('com_ui_delete_prompt')}
|
||||
className="w-11/12 max-w-md"
|
||||
main={<Label>{localize('com_ui_prompt_delete_confirm', { 0: group.name })}</Label>}
|
||||
selection={
|
||||
<Button onClick={handleDelete} variant="destructive" disabled={deleteGroup.isLoading}>
|
||||
{deleteGroup.isLoading ? <Spinner /> : localize('com_ui_delete')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</OGDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,223 +0,0 @@
|
|||
import { memo, useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { EarthIcon, Pencil, Trash2, User } from 'lucide-react';
|
||||
import { PermissionBits, ResourceType, type TPromptGroup } from 'librechat-data-provider';
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
Button,
|
||||
Spinner,
|
||||
OGDialog,
|
||||
TooltipAnchor,
|
||||
OGDialogTrigger,
|
||||
OGDialogTemplate,
|
||||
useToastContext,
|
||||
} from '@librechat/client';
|
||||
import { useLocalize, useAuthContext, useResourcePermissions } from '~/hooks';
|
||||
import { useLiveAnnouncer } from '~/Providers';
|
||||
import { useDeletePromptGroup, useUpdatePromptGroup } from '~/data-provider';
|
||||
import CategoryIcon from '../utils/CategoryIcon';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
function DashGroupItemComponent({ group }: { group: TPromptGroup }) {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
const { user } = useAuthContext();
|
||||
|
||||
const isSharedPrompt = group.author !== user?.id && Boolean(group.authorName);
|
||||
|
||||
const { showToast } = useToastContext();
|
||||
const { announcePolite } = useLiveAnnouncer();
|
||||
const [nameInputValue, setNameInputValue] = useState(group.name);
|
||||
const [renameOpen, setRenameOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!renameOpen) {
|
||||
setNameInputValue(group.name);
|
||||
}
|
||||
}, [group.name, renameOpen]);
|
||||
|
||||
const { hasPermission } = useResourcePermissions(ResourceType.PROMPTGROUP, group._id || '');
|
||||
const canEdit = hasPermission(PermissionBits.EDIT);
|
||||
const canDelete = hasPermission(PermissionBits.DELETE);
|
||||
|
||||
const isGlobalGroup = group.isPublic === true;
|
||||
|
||||
const updateGroup = useUpdatePromptGroup({
|
||||
onSuccess: () => {
|
||||
setRenameOpen(false);
|
||||
showToast({ status: 'success', message: localize('com_ui_prompt_renamed') });
|
||||
announcePolite({ message: localize('com_ui_prompt_renamed'), isStatus: true });
|
||||
},
|
||||
onError: () => {
|
||||
showToast({ status: 'error', message: localize('com_ui_prompt_update_error') });
|
||||
},
|
||||
});
|
||||
|
||||
const deleteGroup = useDeletePromptGroup({
|
||||
onSuccess: (_response, variables) => {
|
||||
announcePolite({
|
||||
message: localize('com_ui_prompt_deleted_group', { 0: group.name }),
|
||||
isStatus: true,
|
||||
});
|
||||
if (variables.id === group._id) {
|
||||
navigate('/d/prompts');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { isLoading: isSaving } = updateGroup;
|
||||
const isDeleting = deleteGroup.isLoading;
|
||||
|
||||
const updateGroupRef = useRef(updateGroup);
|
||||
updateGroupRef.current = updateGroup;
|
||||
const deleteGroupRef = useRef(deleteGroup);
|
||||
deleteGroupRef.current = deleteGroup;
|
||||
|
||||
const handleSaveRename = useCallback(() => {
|
||||
updateGroupRef.current.mutate({ id: group._id ?? '', payload: { name: nameInputValue } });
|
||||
}, [group._id, nameInputValue]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
deleteGroupRef.current.mutate({ id: group._id ?? '' });
|
||||
}, [group._id]);
|
||||
|
||||
const handleContainerClick = useCallback(() => {
|
||||
navigate(`/d/prompts/${group._id}`, { replace: true });
|
||||
}, [group._id, navigate]);
|
||||
|
||||
const ariaLabel = group.category
|
||||
? localize('com_ui_prompt_group_button', {
|
||||
name: group.name,
|
||||
category: group.category,
|
||||
})
|
||||
: localize('com_ui_prompt_group_button_no_category', {
|
||||
name: group.name,
|
||||
});
|
||||
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
'group/card relative flex w-full items-center overflow-hidden rounded-lg border border-border-light bg-transparent text-left hover:bg-surface-secondary',
|
||||
params.promptId === group._id && 'bg-surface-hover',
|
||||
)}
|
||||
>
|
||||
<div className="flex w-0 min-w-0 flex-1 items-center gap-2 overflow-hidden p-4">
|
||||
<CategoryIcon
|
||||
category={group.category ?? ''}
|
||||
className="icon-lg shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<a
|
||||
href={`/d/prompts/${group._id}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleContainerClick();
|
||||
}}
|
||||
className="min-w-0 flex-1 truncate text-base font-semibold text-text-primary after:absolute after:inset-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary focus-visible:ring-offset-2"
|
||||
title={group.name}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{group.name}
|
||||
</a>
|
||||
{isSharedPrompt && (
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_by_author', { 0: group.authorName })}
|
||||
side="top"
|
||||
render={
|
||||
<span
|
||||
tabIndex={0}
|
||||
role="img"
|
||||
aria-label={localize('com_ui_by_author', { 0: group.authorName })}
|
||||
className="flex shrink-0 cursor-default items-center rounded-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
|
||||
>
|
||||
<User className="icon-md text-text-secondary" aria-hidden="true" />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isGlobalGroup && (
|
||||
<EarthIcon
|
||||
className="icon-md shrink-0 text-green-400"
|
||||
aria-label={localize('com_ui_global_group')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex shrink-0 items-center gap-1 pr-2">
|
||||
{canEdit && (
|
||||
<OGDialog open={renameOpen} onOpenChange={setRenameOpen}>
|
||||
<OGDialogTrigger asChild>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_rename')}
|
||||
side="top"
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
aria-label={localize('com_ui_rename_prompt_name', { name: group.name })}
|
||||
>
|
||||
<Pencil className="size-4 text-text-primary" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_rename_prompt')}
|
||||
className="w-11/12 max-w-md"
|
||||
main={
|
||||
<Input
|
||||
value={nameInputValue}
|
||||
onChange={(e) => setNameInputValue(e.target.value)}
|
||||
className="w-full"
|
||||
aria-label={localize('com_ui_rename_prompt_name', { name: group.name })}
|
||||
/>
|
||||
}
|
||||
selection={
|
||||
<Button onClick={handleSaveRename} variant="submit">
|
||||
{isSaving ? <Spinner /> : localize('com_ui_save')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</OGDialog>
|
||||
)}
|
||||
|
||||
{canDelete && (
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_delete')}
|
||||
side="top"
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
aria-label={localize('com_ui_delete_prompt_name', { name: group.name })}
|
||||
>
|
||||
<Trash2 className="size-4 text-text-primary" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
title={localize('com_ui_delete_prompt')}
|
||||
className="w-11/12 max-w-md"
|
||||
main={<Label>{localize('com_ui_prompt_delete_confirm', { 0: group.name })}</Label>}
|
||||
selection={
|
||||
<Button onClick={handleDelete} variant="destructive">
|
||||
{isDeleting ? <Spinner /> : localize('com_ui_delete')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</OGDialog>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DashGroupItemComponent);
|
||||
|
|
@ -1,65 +1,44 @@
|
|||
import { FileText } from 'lucide-react';
|
||||
import { Skeleton } from '@librechat/client';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import DashGroupItem from './DashGroupItem';
|
||||
import ChatGroupItem from './ChatGroupItem';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function List({
|
||||
groups = [],
|
||||
isChatRoute,
|
||||
isLoading,
|
||||
isChatRoute,
|
||||
}: {
|
||||
groups?: TPromptGroup[];
|
||||
isChatRoute: boolean;
|
||||
isLoading: boolean;
|
||||
isChatRoute?: boolean;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<section className="flex-grow overflow-y-auto" aria-label={localize('com_ui_prompt_groups')}>
|
||||
<div className="overflow-y-auto overflow-x-hidden">
|
||||
{isLoading && isChatRoute && (
|
||||
<Skeleton className="my-2 flex h-[84px] w-full rounded-2xl border-0 px-3 pb-4 pt-3" />
|
||||
)}
|
||||
{isLoading && !isChatRoute && (
|
||||
<div className="space-y-2 px-2">
|
||||
{Array.from({ length: 10 }).map((_, index: number) => (
|
||||
<Skeleton key={index} className="flex h-14 w-full rounded-lg border-0 p-4" />
|
||||
))}
|
||||
<section className="flex-grow" aria-label={localize('com_ui_prompt_groups')}>
|
||||
<div>
|
||||
{isLoading &&
|
||||
Array.from({ length: 3 }, (_, i) => (
|
||||
<Skeleton key={i} className="mb-1.5 h-[72px] w-full rounded-xl" />
|
||||
))}
|
||||
{!isLoading && groups.length === 0 && (
|
||||
<div className="my-2 flex flex-col items-center justify-center rounded-lg border border-border-medium bg-transparent p-6 text-center">
|
||||
<div className="mb-2 flex size-10 items-center justify-center rounded-full bg-surface-tertiary">
|
||||
<FileText className="size-5 text-text-secondary" aria-hidden="true" />
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && groups.length === 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center rounded-lg border border-border-light bg-transparent p-6 text-center',
|
||||
isChatRoute ? 'my-2' : 'mx-2 my-4',
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex size-10 items-center justify-center rounded-full bg-surface-tertiary">
|
||||
<FileText className="size-5 text-text-secondary" aria-hidden="true" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-text-primary">
|
||||
{localize('com_ui_no_prompts_title')}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-text-secondary">
|
||||
{localize('com_ui_add_first_prompt')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isChatRoute ? (
|
||||
groups.map((group) => <ChatGroupItem key={group._id} group={group} />)
|
||||
) : (
|
||||
<div className="space-y-2 px-0 md:px-2">
|
||||
{groups.map((group) => (
|
||||
<DashGroupItem key={group._id} group={group} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-text-primary">
|
||||
{localize('com_ui_no_prompts_title')}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-text-secondary">
|
||||
{localize('com_ui_add_first_prompt')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{groups.map((group) => (
|
||||
<ChatGroupItem key={group._id} group={group} isChatRoute={isChatRoute} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default function NoPromptGroup() {
|
|||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => {
|
||||
navigate('/d/prompts');
|
||||
navigate('/prompts');
|
||||
}}
|
||||
aria-label={localize('com_ui_back_to_prompts')}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
export { default as List } from './List';
|
||||
export { default as ListCard } from './ListCard';
|
||||
export { default as DashGroupItem } from './DashGroupItem';
|
||||
export { default as ChatGroupItem } from './ChatGroupItem';
|
||||
export { default as NoPromptGroup } from './NoPromptGroup';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export default function FilterPrompts({
|
|||
dropdownClassName?: string;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { name, setName, hasAccess, promptGroups } = usePromptGroupsContext();
|
||||
const { name, setName, hasAccess, promptGroups } = usePromptGroupsContext() ?? {};
|
||||
const { categories } = useCategories({ className: 'h-4 w-4', hasAccess });
|
||||
const [searchTerm, setSearchTerm] = useState(name || '');
|
||||
const [categoryFilter, setCategory] = useRecoilState(store.promptsCategory);
|
||||
|
|
@ -77,6 +77,9 @@ export default function FilterPrompts({
|
|||
}, [name]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!setName) {
|
||||
return;
|
||||
}
|
||||
setName(debouncedSearchTerm);
|
||||
}, [debouncedSearchTerm, setName]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,78 +1,38 @@
|
|||
import { useMemo, useCallback } from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button, Sidebar, TooltipAnchor } from '@librechat/client';
|
||||
import { usePromptGroupsContext, useDashboardContext } from '~/Providers';
|
||||
import { useLocalize, useCustomLink } from '~/hooks';
|
||||
import ManagePrompts from '../buttons/ManagePrompts';
|
||||
import { usePromptGroupsContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import PanelNavigation from './PanelNavigation';
|
||||
import List from '../lists/List';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
export default function GroupSidePanel({
|
||||
children,
|
||||
className = '',
|
||||
closePanelRef,
|
||||
onClose,
|
||||
isChatRoute: isChatRouteProp,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
closePanelRef?: React.RefObject<HTMLButtonElement>;
|
||||
onClose?: () => void;
|
||||
isChatRoute?: boolean;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const localize = useLocalize();
|
||||
const isChatRoute = useMemo(() => location.pathname?.startsWith('/c/'), [location.pathname]);
|
||||
const isChatRoute = isChatRouteProp ?? location.pathname?.startsWith('/c/') ?? false;
|
||||
|
||||
const { prevLocationPath } = useDashboardContext();
|
||||
const setPromptsName = useSetRecoilState(store.promptsName);
|
||||
const setPromptsCategory = useSetRecoilState(store.promptsCategory);
|
||||
const clickCallback = useCallback(() => {
|
||||
setPromptsName('');
|
||||
setPromptsCategory('');
|
||||
}, [setPromptsName, setPromptsCategory]);
|
||||
const lastConversationId = useMemo(() => {
|
||||
if (!prevLocationPath || prevLocationPath.includes('/d/')) {
|
||||
return 'new';
|
||||
}
|
||||
const parts = prevLocationPath.split('/');
|
||||
return parts[parts.length - 1];
|
||||
}, [prevLocationPath]);
|
||||
const chatLinkHandler = useCustomLink('/c/' + lastConversationId, clickCallback);
|
||||
const promptsLinkHandler = useCustomLink('/d/prompts');
|
||||
|
||||
const { promptGroups, groupsQuery, nextPage, prevPage, hasNextPage, hasPreviousPage } =
|
||||
usePromptGroupsContext();
|
||||
const context = usePromptGroupsContext();
|
||||
if (!context) {
|
||||
return null;
|
||||
}
|
||||
const { promptGroups, groupsQuery, nextPage, prevPage, hasNextPage, hasPreviousPage } = context;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="prompts-panel"
|
||||
className={cn('flex h-full w-full flex-col md:mr-2 md:w-[450px] md:shrink-0', className)}
|
||||
>
|
||||
<div id="prompts-panel" className={cn('flex h-full w-full flex-col', className)}>
|
||||
{onClose && (
|
||||
<div className="flex items-center justify-between px-2 py-[2px] md:py-2">
|
||||
<nav aria-label="Breadcrumb" className="flex items-center gap-1.5 text-sm">
|
||||
<a
|
||||
href="/"
|
||||
onClick={chatLinkHandler}
|
||||
className="flex items-center gap-1 text-text-secondary hover:text-text-primary"
|
||||
>
|
||||
<ArrowLeft className="icon-xs" aria-hidden="true" />
|
||||
<span>{localize('com_ui_chat')}</span>
|
||||
</a>
|
||||
<span className="text-text-tertiary" aria-hidden="true">
|
||||
/
|
||||
</span>
|
||||
<a
|
||||
href="/d/prompts"
|
||||
onClick={promptsLinkHandler}
|
||||
className="text-text-secondary hover:text-text-primary"
|
||||
>
|
||||
{localize('com_ui_prompts')}
|
||||
</a>
|
||||
</nav>
|
||||
<div className="flex items-center justify-end px-2 py-[2px] md:py-2">
|
||||
<TooltipAnchor
|
||||
description={localize('com_nav_close_sidebar')}
|
||||
render={
|
||||
|
|
@ -92,27 +52,31 @@ export default function GroupSidePanel({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2 overflow-hidden">
|
||||
{children}
|
||||
<div className={cn('relative flex h-full flex-col', isChatRoute ? '' : 'px-2 md:px-0')}>
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
<div className="scrollbar-gutter-stable flex h-full min-h-0 flex-col gap-2 overflow-y-auto overflow-x-hidden pl-3 pr-1 text-text-primary">
|
||||
<div className="shrink-0 space-y-2">{children}</div>
|
||||
<List
|
||||
groups={promptGroups}
|
||||
isChatRoute={isChatRoute}
|
||||
isLoading={!!groupsQuery.isLoading}
|
||||
isChatRoute={isChatRoute}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(isChatRoute ? '' : 'px-2 pb-3 pt-2 md:px-0')}>
|
||||
<PanelNavigation
|
||||
onPrevious={prevPage}
|
||||
onNext={nextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
hasPreviousPage={hasPreviousPage}
|
||||
isLoading={groupsQuery.isFetching}
|
||||
isChatRoute={isChatRoute}
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none inset-x-0 bottom-0 bg-gradient-to-t from-surface-primary-alt from-60% to-transparent px-3 pb-2',
|
||||
)}
|
||||
>
|
||||
{isChatRoute && <ManagePrompts className="select-none" />}
|
||||
</PanelNavigation>
|
||||
<div className="pointer-events-auto">
|
||||
<PanelNavigation
|
||||
onPrevious={prevPage}
|
||||
onNext={nextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
hasPreviousPage={hasPreviousPage}
|
||||
isLoading={groupsQuery.isFetching}
|
||||
isChatRoute={isChatRoute}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { memo } from 'react';
|
||||
import { Button, ThemeSelector } from '@librechat/client';
|
||||
import { Button } from '@librechat/client';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
function PanelNavigation({
|
||||
|
|
@ -8,7 +8,6 @@ function PanelNavigation({
|
|||
hasNextPage,
|
||||
hasPreviousPage,
|
||||
isLoading,
|
||||
isChatRoute,
|
||||
children,
|
||||
}: {
|
||||
onPrevious: () => void;
|
||||
|
|
@ -23,10 +22,7 @@ function PanelNavigation({
|
|||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2 pl-1">
|
||||
{!isChatRoute && <ThemeSelector returnThemeOnly={true} />}
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex gap-2 pl-1">{children}</div>
|
||||
<nav className="flex items-center gap-2" aria-label={localize('com_ui_pagination')}>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
|
|||
|
|
@ -1,21 +1,17 @@
|
|||
import { usePromptGroupsContext } from '~/Providers';
|
||||
import { SystemRoles } from 'librechat-data-provider';
|
||||
import { useAuthContext } from '~/hooks';
|
||||
import { AdminSettings } from '~/components/Prompts';
|
||||
import AutoSendPrompt from '../buttons/AutoSendPrompt';
|
||||
import PromptSidePanel from './GroupSidePanel';
|
||||
import FilterPrompts from './FilterPrompts';
|
||||
|
||||
export default function PromptsAccordion() {
|
||||
const groupsNav = usePromptGroupsContext();
|
||||
const { user } = useAuthContext();
|
||||
return (
|
||||
<div className="flex h-auto w-full flex-col px-3 pb-3">
|
||||
<PromptSidePanel
|
||||
className="h-auto space-y-2 md:mr-0 md:min-w-0 lg:w-full xl:w-full"
|
||||
{...groupsNav}
|
||||
>
|
||||
<FilterPrompts />
|
||||
<div className="flex w-full items-center justify-end">
|
||||
<AutoSendPrompt />
|
||||
</div>
|
||||
</PromptSidePanel>
|
||||
</div>
|
||||
<PromptSidePanel className="space-y-2">
|
||||
<FilterPrompts />
|
||||
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
|
||||
<AutoSendPrompt />
|
||||
</PromptSidePanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import {
|
||||
Dices,
|
||||
BoxIcon,
|
||||
FileText,
|
||||
PenLineIcon,
|
||||
LightbulbIcon,
|
||||
LineChartIcon,
|
||||
|
|
@ -9,7 +10,6 @@ import {
|
|||
PlaneTakeoffIcon,
|
||||
GraduationCapIcon,
|
||||
TerminalSquareIcon,
|
||||
// NEW: Add these for agent categories
|
||||
Users as UsersIcon,
|
||||
Beaker as BeakerIcon,
|
||||
Settings as SettingsIcon,
|
||||
|
|
@ -26,7 +26,6 @@ const categoryIconMap: Record<string, React.ElementType> = {
|
|||
code: TerminalSquareIcon,
|
||||
travel: PlaneTakeoffIcon,
|
||||
teach_or_explain: GraduationCapIcon,
|
||||
// NEW: Agent categories
|
||||
general: BoxIcon,
|
||||
hr: UsersIcon,
|
||||
rd: BeakerIcon,
|
||||
|
|
@ -39,13 +38,12 @@ const categoryColorMap: Record<string, string> = {
|
|||
code: 'text-red-500',
|
||||
misc: 'text-blue-300',
|
||||
shop: 'text-purple-400',
|
||||
idea: 'text-yellow-500/90 dark:text-yellow-300 ',
|
||||
idea: 'text-yellow-500/90 dark:text-yellow-300',
|
||||
write: 'text-purple-400',
|
||||
travel: 'text-yellow-500/90 dark:text-yellow-300 ',
|
||||
travel: 'text-yellow-500/90 dark:text-yellow-300',
|
||||
finance: 'text-orange-400',
|
||||
roleplay: 'text-orange-400',
|
||||
teach_or_explain: 'text-blue-300',
|
||||
// NEW: Agent categories
|
||||
general: 'text-blue-500',
|
||||
hr: 'text-green-500',
|
||||
rd: 'text-purple-500',
|
||||
|
|
@ -61,10 +59,7 @@ export default function CategoryIcon({
|
|||
category: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const IconComponent = categoryIconMap[category];
|
||||
const colorClass = categoryColorMap[category] + ' ' + className;
|
||||
if (!IconComponent) {
|
||||
return null;
|
||||
}
|
||||
const IconComponent = categoryIconMap[category] ?? FileText;
|
||||
const colorClass = categoryColorMap[category] ?? 'text-text-secondary';
|
||||
return <IconComponent className={cn('size-4', colorClass, className)} aria-hidden="true" />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import { useCallback, useEffect, useState, useMemo, memo, lazy, Suspense, useRef } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useMediaQuery, NewChatIcon } from '@librechat/client';
|
||||
import { PermissionTypes, Permissions, QueryKeys } from 'librechat-data-provider';
|
||||
import { useMediaQuery } from '@librechat/client';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { InfiniteQueryObserverResult } from '@tanstack/react-query';
|
||||
import type { ConversationListResponse } from 'librechat-data-provider';
|
||||
import type { List } from 'react-virtualized';
|
||||
import {
|
||||
useLocalize,
|
||||
useNewConvo,
|
||||
useHasAccess,
|
||||
useAuthContext,
|
||||
useLocalStorage,
|
||||
|
|
@ -17,18 +15,14 @@ import {
|
|||
import { useConversationsInfiniteQuery, useTitleGeneration } from '~/data-provider';
|
||||
import { Conversations } from '~/components/Conversations';
|
||||
import SearchBar from '~/components/Nav/SearchBar';
|
||||
import { clearMessagesCache } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const BookmarkNav = lazy(() => import('~/components/Nav/Bookmarks/BookmarkNav'));
|
||||
|
||||
const ConversationsSection = memo(() => {
|
||||
const localize = useLocalize();
|
||||
const queryClient = useQueryClient();
|
||||
const { newConversation } = useNewConvo();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const setSidebarExpanded = useSetRecoilState(store.sidebarExpanded);
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||
const { isAuthenticated } = useAuthContext();
|
||||
useTitleGeneration(isAuthenticated);
|
||||
|
||||
|
|
@ -122,32 +116,6 @@ const ConversationsSection = memo(() => {
|
|||
)}
|
||||
{search.enabled && <SearchBar isSmallScreen={isSmallScreen} />}
|
||||
</div>
|
||||
{isSmallScreen && (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={localize('com_ui_new_chat')}
|
||||
className="flex w-full cursor-pointer items-center rounded-lg px-2.5 py-2 text-sm text-text-primary outline-none hover:bg-surface-active-alt focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-black dark:focus-visible:ring-white"
|
||||
onClick={() => {
|
||||
clearMessagesCache(queryClient, conversation?.conversationId);
|
||||
queryClient.invalidateQueries([QueryKeys.messages]);
|
||||
newConversation();
|
||||
setSidebarExpanded(false);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
clearMessagesCache(queryClient, conversation?.conversationId);
|
||||
queryClient.invalidateQueries([QueryKeys.messages]);
|
||||
newConversation();
|
||||
setSidebarExpanded(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<NewChatIcon className="mr-2 h-5 w-5" />
|
||||
<span className="truncate">{localize('com_ui_new_chat')}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex min-h-0 flex-grow flex-col overflow-hidden">
|
||||
<Conversations
|
||||
conversations={conversations}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { memo, useCallback, lazy, Suspense } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SquarePen } from 'lucide-react';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { Skeleton, Sidebar, Button, TooltipAnchor, NewChatIcon } from '@librechat/client';
|
||||
import { Skeleton, Sidebar, Button, TooltipAnchor } from '@librechat/client';
|
||||
import type { NavLink } from '~/common';
|
||||
import { CLOSE_SIDEBAR_ID } from '~/components/Chat/Menus/OpenSidebar';
|
||||
import { useActivePanel, resolveActivePanel } from '~/Providers';
|
||||
|
|
@ -20,13 +21,12 @@ const NewChatButton = memo(function NewChatButton() {
|
|||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (e.button === 0 && (e.ctrlKey || e.metaKey)) {
|
||||
return;
|
||||
if (e.button === 0 && !e.ctrlKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
clearMessagesCache(queryClient, conversation?.conversationId);
|
||||
queryClient.invalidateQueries([QueryKeys.messages]);
|
||||
newConversation();
|
||||
}
|
||||
e.preventDefault();
|
||||
clearMessagesCache(queryClient, conversation?.conversationId);
|
||||
queryClient.invalidateQueries([QueryKeys.messages]);
|
||||
newConversation();
|
||||
},
|
||||
[queryClient, conversation?.conversationId, newConversation],
|
||||
);
|
||||
|
|
@ -43,9 +43,7 @@ const NewChatButton = memo(function NewChatButton() {
|
|||
className="flex h-9 w-9 items-center justify-center rounded-lg transition-colors hover:bg-surface-hover"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="flex size-6 items-center justify-center rounded-full bg-text-primary">
|
||||
<NewChatIcon className="size-3.5 text-white dark:text-black" />
|
||||
</div>
|
||||
<SquarePen className="h-5 w-5 text-text-primary" />
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
|
|
@ -99,7 +97,7 @@ const NavIconButton = memo(function NavIconButton({
|
|||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<link.icon className="h-4 w-4" aria-hidden="true" />
|
||||
<link.icon className="h-5 w-5" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
|
@ -145,6 +143,7 @@ function ExpandedPanel({
|
|||
}
|
||||
/>
|
||||
<NewChatButton />
|
||||
<div className="mx-2 border-b border-border-light" />
|
||||
<div className="flex flex-col gap-1 overflow-y-auto">
|
||||
{links.map((link) => (
|
||||
<NavIconButton
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ function UnifiedSidebar() {
|
|||
width: 'min(85vw, 380px)',
|
||||
transition: `transform ${TRANSITION_MS}ms ${EASING}`,
|
||||
}}
|
||||
{...{ inert: !expanded ? '' : undefined }}
|
||||
inert={!expanded ? '' : undefined}
|
||||
>
|
||||
<SidebarChatProvider>
|
||||
<ActivePanelProvider>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@ import { SESSION_KEY, isSafeRedirect, getPostLoginRedirect } from '~/utils';
|
|||
import useTimeout from './useTimeout';
|
||||
import store from '~/store';
|
||||
|
||||
const AuthContext = createContext<TAuthContext | undefined>(undefined);
|
||||
const AuthContext = (import.meta.hot?.data?.__AuthContext ??
|
||||
createContext<TAuthContext | undefined>(undefined)) as React.Context<TAuthContext | undefined>;
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.__AuthContext = AuthContext;
|
||||
}
|
||||
|
||||
const AuthContextProvider = ({
|
||||
authConfig,
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ import {
|
|||
useGetAllEffectivePermissionsQuery,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import type { TUpdateUserPlugins, TPlugin, MCPServersResponse } from 'librechat-data-provider';
|
||||
import type { MCPServerInitState } from '~/store/mcp';
|
||||
import type { ConfigFieldDetail } from '~/common';
|
||||
import { useLocalize, useHasAccess, useMCPSelect, useMCPConnectionStatus } from '~/hooks';
|
||||
import { useGetStartupConfig, useMCPServersQuery } from '~/data-provider';
|
||||
import { mcpServerInitStatesAtom, getServerInitState } from '~/store/mcp';
|
||||
import type { MCPServerInitState } from '~/store/mcp';
|
||||
|
||||
export interface MCPServerDefinition {
|
||||
serverName: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Blocks, MCPIcon, AttachmentIcon } from '@librechat/client';
|
||||
import { Database, Bookmark, Settings2, ArrowRightToLine, MessageSquareQuote } from 'lucide-react';
|
||||
import { MCPIcon, AttachmentIcon, OpenAIMinimalIcon } from '@librechat/client';
|
||||
import {
|
||||
Bot,
|
||||
Brain,
|
||||
Bookmark,
|
||||
NotebookPen,
|
||||
ArrowRightToLine,
|
||||
SlidersHorizontal,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Permissions,
|
||||
EModelEndpoint,
|
||||
|
|
@ -74,6 +81,22 @@ export default function useSideNavLinks({
|
|||
|
||||
const Links = useMemo(() => {
|
||||
const links: NavLink[] = [];
|
||||
|
||||
if (
|
||||
endpointsConfig?.[EModelEndpoint.agents] &&
|
||||
hasAccessToAgents &&
|
||||
hasAccessToCreateAgents &&
|
||||
endpointsConfig[EModelEndpoint.agents].disableBuilder !== true
|
||||
) {
|
||||
links.push({
|
||||
title: 'com_sidepanel_agent_builder',
|
||||
label: '',
|
||||
icon: Bot,
|
||||
id: EModelEndpoint.agents,
|
||||
Component: AgentPanelSwitch,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isAssistantsEndpoint(endpoint) &&
|
||||
((endpoint === EModelEndpoint.assistants &&
|
||||
|
|
@ -87,32 +110,17 @@ export default function useSideNavLinks({
|
|||
links.push({
|
||||
title: 'com_sidepanel_assistant_builder',
|
||||
label: '',
|
||||
icon: Blocks,
|
||||
icon: OpenAIMinimalIcon,
|
||||
id: EModelEndpoint.assistants,
|
||||
Component: PanelSwitch,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
endpointsConfig?.[EModelEndpoint.agents] &&
|
||||
hasAccessToAgents &&
|
||||
hasAccessToCreateAgents &&
|
||||
endpointsConfig[EModelEndpoint.agents].disableBuilder !== true
|
||||
) {
|
||||
links.push({
|
||||
title: 'com_sidepanel_agent_builder',
|
||||
label: '',
|
||||
icon: Blocks,
|
||||
id: EModelEndpoint.agents,
|
||||
Component: AgentPanelSwitch,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasAccessToPrompts) {
|
||||
links.push({
|
||||
title: 'com_ui_prompts',
|
||||
label: '',
|
||||
icon: MessageSquareQuote,
|
||||
icon: NotebookPen,
|
||||
id: 'prompts',
|
||||
Component: PromptsAccordion,
|
||||
});
|
||||
|
|
@ -122,24 +130,19 @@ export default function useSideNavLinks({
|
|||
links.push({
|
||||
title: 'com_ui_memories',
|
||||
label: '',
|
||||
icon: Database,
|
||||
icon: Brain,
|
||||
id: 'memories',
|
||||
Component: MemoryPanel,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
interfaceConfig.parameters === true &&
|
||||
isParamEndpoint(endpoint ?? '', endpointType ?? '') === true &&
|
||||
!isAgentsEndpoint(endpoint) &&
|
||||
keyProvided
|
||||
) {
|
||||
if (hasAccessToBookmarks) {
|
||||
links.push({
|
||||
title: 'com_sidepanel_parameters',
|
||||
title: 'com_sidepanel_conversation_tags',
|
||||
label: '',
|
||||
icon: Settings2,
|
||||
id: 'parameters',
|
||||
Component: Parameters,
|
||||
icon: Bookmark,
|
||||
id: 'bookmarks',
|
||||
Component: BookmarkPanel,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -151,13 +154,18 @@ export default function useSideNavLinks({
|
|||
Component: FilesPanel,
|
||||
});
|
||||
|
||||
if (hasAccessToBookmarks) {
|
||||
if (
|
||||
interfaceConfig.parameters === true &&
|
||||
isParamEndpoint(endpoint ?? '', endpointType ?? '') === true &&
|
||||
!isAgentsEndpoint(endpoint) &&
|
||||
keyProvided
|
||||
) {
|
||||
links.push({
|
||||
title: 'com_sidepanel_conversation_tags',
|
||||
title: 'com_sidepanel_parameters',
|
||||
label: '',
|
||||
icon: Bookmark,
|
||||
id: 'bookmarks',
|
||||
Component: BookmarkPanel,
|
||||
icon: SlidersHorizontal,
|
||||
id: 'parameters',
|
||||
Component: Parameters,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import { MessagesSquare } from 'lucide-react';
|
||||
import { useUserKeyQuery } from 'librechat-data-provider/react-query';
|
||||
import { getConfigDefaults, getEndpointField } from 'librechat-data-provider';
|
||||
import type { TEndpointsConfig } from 'librechat-data-provider';
|
||||
|
|
@ -53,7 +53,7 @@ export default function useUnifiedSidebarLinks() {
|
|||
const conversationLink: NavLink = {
|
||||
title: 'com_ui_chat_history',
|
||||
label: '',
|
||||
icon: MessageSquare,
|
||||
icon: MessagesSquare,
|
||||
id: 'conversations',
|
||||
Component: ConversationsSection,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -400,6 +400,8 @@
|
|||
"com_info_heic_converting": "Converting HEIC image to JPEG...",
|
||||
"com_nav_2fa": "Two-Factor Authentication (2FA)",
|
||||
"com_nav_account_settings": "Account Settings",
|
||||
"com_nav_advanced_prompts": "Advanced prompts editor",
|
||||
"com_nav_advanced_prompts_desc": "Enable versioning and production control for prompts",
|
||||
"com_nav_always_make_prod": "Always make new prompt versions production",
|
||||
"com_nav_archive_created_at": "Date Archived",
|
||||
"com_nav_archive_name": "Name",
|
||||
|
|
@ -595,7 +597,6 @@
|
|||
"com_nav_theme_dark": "Dark",
|
||||
"com_nav_theme_light": "Light",
|
||||
"com_nav_theme_system": "System",
|
||||
"com_nav_toggle_sidebar": "Toggle sidebar",
|
||||
"com_nav_tool_dialog": "Assistant Tools",
|
||||
"com_nav_tool_dialog_agents": "Agent Tools",
|
||||
"com_nav_tool_dialog_description": "Assistant must be saved to persist tool selections.",
|
||||
|
|
@ -770,7 +771,6 @@
|
|||
"com_ui_azure_ad": "Entra ID",
|
||||
"com_ui_back": "Back",
|
||||
"com_ui_back_to_builder": "Back to builder",
|
||||
"com_ui_back_to_chat": "Back to Chat",
|
||||
"com_ui_back_to_prompts": "Back to Prompts",
|
||||
"com_ui_backup_code_number": "Code #{{number}}",
|
||||
"com_ui_backup_codes": "Backup Codes",
|
||||
|
|
@ -889,7 +889,6 @@
|
|||
"com_ui_custom_header_name": "Custom Header Name",
|
||||
"com_ui_custom_prompt_mode": "Custom Prompt Mode",
|
||||
"com_ui_dark_theme_enabled": "Dark theme enabled",
|
||||
"com_ui_dashboard": "Dashboard",
|
||||
"com_ui_date": "Date",
|
||||
"com_ui_date_april": "April",
|
||||
"com_ui_date_august": "August",
|
||||
|
|
@ -928,7 +927,6 @@
|
|||
"com_ui_delete_not_allowed": "Delete operation is not allowed",
|
||||
"com_ui_delete_preset": "Delete Preset?",
|
||||
"com_ui_delete_prompt": "Delete Prompt?",
|
||||
"com_ui_delete_prompt_name": "Delete Prompt - {{name}}",
|
||||
"com_ui_delete_shared_link": "Delete shared link?",
|
||||
"com_ui_delete_shared_link_heading": "Delete Shared Link",
|
||||
"com_ui_delete_success": "Successfully deleted",
|
||||
|
|
@ -1054,7 +1052,6 @@
|
|||
"com_ui_generating_image": "Generating image...",
|
||||
"com_ui_generation_settings": "Generation Settings",
|
||||
"com_ui_getting_started": "Getting Started",
|
||||
"com_ui_global_group": "Global prompt",
|
||||
"com_ui_go_back": "Go back",
|
||||
"com_ui_go_to_conversation": "Go to conversation",
|
||||
"com_ui_good_afternoon": "Good afternoon",
|
||||
|
|
@ -1278,6 +1275,7 @@
|
|||
"com_ui_prompt": "Prompt",
|
||||
"com_ui_prompt_category_selector_aria": "Prompt's category selector",
|
||||
"com_ui_prompt_delete_confirm": "Are you sure you want to delete the '{{0}}' prompt?",
|
||||
"com_ui_prompt_delete_error": "There was an error deleting the prompt",
|
||||
"com_ui_prompt_deleted_group": "Prompt group \"{{0}}\" deleted",
|
||||
"com_ui_prompt_details": "{{name}} prompt details",
|
||||
"com_ui_prompt_group_button": "{{name}} prompt, {{category}} category",
|
||||
|
|
@ -1288,7 +1286,6 @@
|
|||
"com_ui_prompt_name": "Prompt Name",
|
||||
"com_ui_prompt_name_required": "Prompt Name is required",
|
||||
"com_ui_prompt_preview_not_shared": "The author has not allowed collaboration for this prompt.",
|
||||
"com_ui_prompt_renamed": "Prompt renamed successfully",
|
||||
"com_ui_prompt_text": "Text",
|
||||
"com_ui_prompt_text_required": "Text is required",
|
||||
"com_ui_prompt_update_error": "There was an error updating the prompt",
|
||||
|
|
@ -1332,8 +1329,6 @@
|
|||
"com_ui_rename": "Rename",
|
||||
"com_ui_rename_conversation": "Rename Conversation",
|
||||
"com_ui_rename_failed": "Failed to rename conversation",
|
||||
"com_ui_rename_prompt": "Rename Prompt",
|
||||
"com_ui_rename_prompt_name": "Rename Prompt - {{name}}",
|
||||
"com_ui_requires_auth": "Requires Authentication",
|
||||
"com_ui_reset": "Reset",
|
||||
"com_ui_reset_adjustments": "Reset adjustments",
|
||||
|
|
|
|||
|
|
@ -1,80 +1,23 @@
|
|||
import { Navigate } from 'react-router-dom';
|
||||
import {
|
||||
PromptsView,
|
||||
PromptForm,
|
||||
CreatePromptForm,
|
||||
EmptyPromptPreview,
|
||||
} from '~/components/Prompts';
|
||||
import { Navigate, useParams } from 'react-router-dom';
|
||||
import DashboardRoute from './Layouts/Dashboard';
|
||||
|
||||
function PromptsRedirect() {
|
||||
const { '*': splat } = useParams();
|
||||
const target = splat ? `/prompts/${splat}` : '/prompts/new';
|
||||
return <Navigate to={target} replace={true} />;
|
||||
}
|
||||
|
||||
const dashboardRoutes = {
|
||||
path: 'd/*',
|
||||
element: <DashboardRoute />,
|
||||
children: [
|
||||
/*
|
||||
{
|
||||
element: <FileDashboardView />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <EmptyVectorStorePreview />,
|
||||
},
|
||||
{
|
||||
path: ':vectorStoreId',
|
||||
element: <DataTableFilePreview />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'files/*',
|
||||
element: <FilesListView />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <EmptyFilePreview />,
|
||||
},
|
||||
{
|
||||
path: ':fileId',
|
||||
element: <FilePreview />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'vector-stores/*',
|
||||
element: <VectorStoreView />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <EmptyVectorStorePreview />,
|
||||
},
|
||||
{
|
||||
path: ':vectorStoreId',
|
||||
element: <VectorStorePreview />,
|
||||
},
|
||||
],
|
||||
},
|
||||
*/
|
||||
{
|
||||
path: 'prompts/*',
|
||||
element: <PromptsView />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <EmptyPromptPreview />,
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
element: <CreatePromptForm />,
|
||||
},
|
||||
{
|
||||
path: ':promptId',
|
||||
element: <PromptForm />,
|
||||
},
|
||||
],
|
||||
element: <PromptsRedirect />,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to="/d/files" replace={true} />,
|
||||
element: <Navigate to="/c/new" replace={true} />,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
import { useMemo, useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { SystemRoles } from 'librechat-data-provider';
|
||||
import { ArrowLeft, MessageSquareQuote } from 'lucide-react';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbSeparator,
|
||||
} from '@librechat/client';
|
||||
import { useLocalize, useCustomLink, useAuthContext } from '~/hooks';
|
||||
import { AdvancedSwitch, AdminSettings } from '~/components/Prompts';
|
||||
import { useDashboardContext } from '~/Providers';
|
||||
import store from '~/store';
|
||||
|
||||
const promptsPathPattern = /prompts\/(?!new(?:\/|$)).*$/;
|
||||
|
||||
const getConversationId = (prevLocationPath: string) => {
|
||||
if (!prevLocationPath || prevLocationPath.includes('/d/')) {
|
||||
return 'new';
|
||||
}
|
||||
const lastPathnameParts = prevLocationPath.split('/');
|
||||
return lastPathnameParts[lastPathnameParts.length - 1];
|
||||
};
|
||||
|
||||
export default function DashBreadcrumb() {
|
||||
const location = useLocation();
|
||||
const localize = useLocalize();
|
||||
const { user } = useAuthContext();
|
||||
const { prevLocationPath } = useDashboardContext();
|
||||
const lastConversationId = useMemo(() => getConversationId(prevLocationPath), [prevLocationPath]);
|
||||
|
||||
const setPromptsName = useSetRecoilState(store.promptsName);
|
||||
const setPromptsCategory = useSetRecoilState(store.promptsCategory);
|
||||
|
||||
const clickCallback = useCallback(() => {
|
||||
setPromptsName('');
|
||||
setPromptsCategory('');
|
||||
}, [setPromptsName, setPromptsCategory]);
|
||||
|
||||
const chatLinkHandler = useCustomLink('/c/' + lastConversationId, clickCallback);
|
||||
const promptsLinkHandler = useCustomLink('/d/prompts');
|
||||
|
||||
const isPromptsPath = useMemo(
|
||||
() => promptsPathPattern.test(location.pathname),
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mr-2 mt-2 flex h-10 items-center justify-between">
|
||||
<Breadcrumb className="mt-1 px-2 dark:text-gray-200">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="hover:dark:text-white">
|
||||
<BreadcrumbLink
|
||||
href="/"
|
||||
className="flex flex-row items-center gap-1"
|
||||
onClick={chatLinkHandler}
|
||||
>
|
||||
<ArrowLeft className="icon-xs" aria-hidden="true" />
|
||||
<span className="hidden md:flex">{localize('com_ui_back_to_chat')}</span>
|
||||
<span className="flex md:hidden">{localize('com_ui_chat')}</span>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
{/*
|
||||
<BreadcrumbItem className="hover:dark:text-white">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex cursor-default items-center gap-1">
|
||||
<BreadcrumbEllipsis className="h-4 w-4" />
|
||||
<BreadcrumbItem className="hover:dark:text-white">
|
||||
<span className="text-gray-400">{localize('com_ui_dashboard')}</span>
|
||||
</BreadcrumbItem>
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>Documentation</DropdownMenuItem>
|
||||
<DropdownMenuItem>Themes</DropdownMenuItem>
|
||||
<DropdownMenuItem>GitHub</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
*/}
|
||||
<BreadcrumbItem className="hover:dark:text-white">
|
||||
<BreadcrumbLink
|
||||
href="/d/prompts"
|
||||
className="flex flex-row items-center gap-1"
|
||||
onClick={promptsLinkHandler}
|
||||
>
|
||||
<MessageSquareQuote className="h-4 w-4 dark:text-gray-300" aria-hidden="true" />
|
||||
{localize('com_ui_prompts')}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{isPromptsPath && <AdvancedSwitch />}
|
||||
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,36 +1,12 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useAuthContext, usePreviousLocation } from '~/hooks';
|
||||
import { DashboardContext } from '~/Providers';
|
||||
import store from '~/store';
|
||||
import { useAuthContext } from '~/hooks';
|
||||
|
||||
export default function DashboardRoute() {
|
||||
const queryClient = useQueryClient();
|
||||
const { isAuthenticated } = useAuthContext();
|
||||
const prevLocationRef = usePreviousLocation();
|
||||
const clearConvoState = store.useClearConvoState();
|
||||
const [prevLocationPath, setPrevLocationPath] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setPrevLocationPath(prevLocationRef.current?.pathname || '');
|
||||
}, [prevLocationRef]);
|
||||
|
||||
useEffect(() => {
|
||||
queryClient.removeQueries([QueryKeys.messages, 'new']);
|
||||
clearConvoState();
|
||||
}, [queryClient, clearConvoState]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardContext.Provider value={{ prevLocationPath }}>
|
||||
<div className="h-screen w-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
</DashboardContext.Provider>
|
||||
);
|
||||
return <Outlet />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export default function Root() {
|
|||
isSmallScreen && sidebarExpanded ? 'translateX(min(85vw, 380px))' : 'none',
|
||||
transition: 'transform 300ms cubic-bezier(0.2, 0, 0, 1)',
|
||||
}}
|
||||
{...{ inert: isSmallScreen && sidebarExpanded ? '' : undefined }}
|
||||
inert={isSmallScreen && sidebarExpanded ? '' : undefined}
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ const AuthLayout = () => (
|
|||
</AuthContextProvider>
|
||||
);
|
||||
|
||||
const loadInlinePromptsView = () =>
|
||||
import('~/components/Prompts/layouts/InlinePromptsView').then((m) => ({
|
||||
Component: m.default,
|
||||
}));
|
||||
|
||||
const baseEl = document.querySelector('base');
|
||||
const baseHref = baseEl?.getAttribute('href') || '/';
|
||||
|
||||
|
|
@ -111,6 +116,18 @@ export const router = createBrowserRouter(
|
|||
path: 'search',
|
||||
element: <Search />,
|
||||
},
|
||||
{
|
||||
path: 'prompts',
|
||||
element: <Navigate to="/prompts/new" replace={true} />,
|
||||
},
|
||||
{
|
||||
path: 'prompts/new',
|
||||
lazy: loadInlinePromptsView,
|
||||
},
|
||||
{
|
||||
path: 'prompts/:promptId',
|
||||
lazy: loadInlinePromptsView,
|
||||
},
|
||||
{
|
||||
path: 'agents',
|
||||
element: (
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ const localStorageAtoms = {
|
|||
forkSetting: atomWithLocalStorage('forkSetting', ''),
|
||||
splitAtTarget: atomWithLocalStorage('splitAtTarget', false),
|
||||
rememberDefaultFork: atomWithLocalStorage(LocalStorageKeys.REMEMBER_FORK_OPTION, false),
|
||||
showThinking: atomWithLocalStorage('showThinking', false),
|
||||
saveBadgesState: atomWithLocalStorage('saveBadgesState', false),
|
||||
|
||||
// Beta features settings
|
||||
|
|
|
|||
31
client/test/babel-plugin-transform-import-meta-hot.cjs
Normal file
31
client/test/babel-plugin-transform-import-meta-hot.cjs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Babel plugin that replaces `import.meta.hot` with `undefined`.
|
||||
*
|
||||
* `babel-plugin-transform-import-meta` handles standard properties (url,
|
||||
* filename, dirname, resolve) but does NOT handle the Vite-specific `hot`
|
||||
* property. Jest runs in CommonJS/Node where `import.meta` is unavailable,
|
||||
* so any reference to `import.meta.hot` causes a SyntaxError.
|
||||
*
|
||||
* Replacing it with `undefined` makes the HMR guard blocks dead-code:
|
||||
* import.meta.hot?.data → undefined?.data → undefined
|
||||
* if (import.meta.hot) → if (undefined) → skipped
|
||||
*/
|
||||
module.exports = function transformImportMetaHot() {
|
||||
return {
|
||||
name: 'transform-import-meta-hot',
|
||||
visitor: {
|
||||
MemberExpression(path) {
|
||||
const { node } = path;
|
||||
if (
|
||||
node.object.type === 'MetaProperty' &&
|
||||
node.object.meta.name === 'import' &&
|
||||
node.object.property.name === 'meta' &&
|
||||
node.property.type === 'Identifier' &&
|
||||
node.property.name === 'hot'
|
||||
) {
|
||||
path.replaceWith({ type: 'Identifier', name: 'undefined' });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -12,19 +12,19 @@ export default function MCPIcon({ className }: { className?: string }) {
|
|||
<path
|
||||
d="M25 97.8528L92.8823 29.9706C102.255 20.598 117.451 20.598 126.823 29.9706V29.9706C136.196 39.3431 136.196 54.5391 126.823 63.9117L75.5581 115.177"
|
||||
stroke="currentColor"
|
||||
strokeWidth="12"
|
||||
strokeWidth="16"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M76.2653 114.47L126.823 63.9117C136.196 54.5391 151.392 54.5391 160.765 63.9117L161.118 64.2652C170.491 73.6378 170.491 88.8338 161.118 98.2063L99.7248 159.6C96.6006 162.724 96.6006 167.789 99.7248 170.913L112.331 183.52"
|
||||
stroke="currentColor"
|
||||
strokeWidth="12"
|
||||
strokeWidth="16"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M109.853 46.9411L59.6482 97.1457C50.2757 106.518 50.2757 121.714 59.6482 131.087V131.087C69.0208 140.459 84.2168 140.459 93.5894 131.087L143.794 80.8822"
|
||||
stroke="currentColor"
|
||||
strokeWidth="12"
|
||||
strokeWidth="16"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -539,6 +539,7 @@ export type TPromptGroup = {
|
|||
productionPrompt?: Pick<TPrompt, 'prompt'> | null;
|
||||
author: string;
|
||||
authorName: string;
|
||||
isPublic?: boolean;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
_id?: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue