mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🎨 style: Prompt UI Refresh & A11Y Improvements (#5614)
* 🚀 feat: Add animated search input and improve filtering UI * 🏄 refactor: Clean up category options and optimize event handlers in ChatGroupItem * 🚀 refactor: 'Rename Prompt' option and enhance prompt filtering UI Changed the useUpdatePromptGroup mutation in prompts.ts to replace the JSON.parse(JSON.stringify(...)) clones with structuredClone. This avoids errors when data contains non‑JSON values and improves data cloning reliability * 🔧 refactor: Update Sharing Prompts UI; fix: Show info message only after updating switch status * 🔧 refactor: Simplify condition checks and replace button with custom Button component in SharePrompt * 🔧 refactor: Update DashGroupItem styles and improve accessibility with updated aria-label * 🔧 refactor: Adjust layout styles in GroupSidePanel and enhance loading skeletons in List component * 🔧 refactor: Improve layout and styling of AdvancedSwitch component; adjust DashBreadcrumb margin for better alignment * 🔧 refactor: Add new surface colors for destructive actions and update localization strings for confirmation prompts * 🔧 refactor: Update PromptForm and PromptName components for improved layout and styling; replace button with custom Button component * 🔧 refactor: Enhance styling and layout of DashGroupItem, FilterPrompts, and Label components for improved user experience * 🔧 refactor: Update DeleteBookmarkButton and Label components for improved layout and text handling * 🔧 refactor: Simplify CategorySelector usage and update destructive surface colors for a11y * 🔧 refactor: Update styling and layout of PromptName, SharePrompt, and DashGroupItem components; enhance Dropdown functionality with custom renderValue * 🔧 refactor: Improve layout and styling of various components; update button sizes and localization strings for better accessibility and user experience * 🔧 refactor: Add useCurrentPromptData hook and enhance RightPanel component; update CategorySelector for improved functionality and accessibility * 🔧 refactor: Update input components and styling for Command and Description; enhance layout and accessibility in PromptVariables and PromptForm * 🔧 refactor: Remove useCurrentPromptData hook and clean up related components; enhance PromptVersions layout * 🔧 refactor: Enhance accessibility by adding aria-labels to buttons and inputs; improve localization for filter prompts * 🔧 refactor: Enhance accessibility by adding aria-labels to various components; improve layout and styling in PromptForm and CategorySelector * 🔧 refactor: Enhance accessibility by adding aria-labels to buttons and components; improve dialog roles and descriptions in SharePrompt and PromptForm * 🔧 refactor: Improve accessibility by adding aria-labels and roles; enhance layout and styling in ChatGroupItem, ListCard, and ManagePrompts components * 🔧 refactor: Update UI components for improved styling and accessibility; replace button elements with custom Button component and enhance layout in VariableForm, PromptDetails, and PromptVariables * 🔧 refactor: Improve null checks for group and instanceProjectId in SharePrompt component; enhance readability and maintainability * style: Enhance AnimatedSearchInput component with TypeScript types; improve conditional rendering for search states and accessibility --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
a44f5b4b6e
commit
73fe0835cf
41 changed files with 1269 additions and 1028 deletions
|
|
@ -1,10 +1,6 @@
|
|||
const { logger } = require('~/config');
|
||||
// const { Categories } = require('./schema/categories');
|
||||
const options = [
|
||||
{
|
||||
label: '',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
label: 'idea',
|
||||
value: 'idea',
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ const DeleteBookmarkButton: FC<{
|
|||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_bookmarks_delete')}
|
||||
className="max-w-[450px]"
|
||||
className="w-11/12 max-w-lg"
|
||||
main={
|
||||
<Label className="text-left text-sm font-medium">
|
||||
{localize('com_ui_bookmark_delete_confirm')} {bookmark}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
import * as Ariakit from '@ariakit/react';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import { useMemo, useEffect, useState } from 'react';
|
||||
import { ShieldEllipsis } from 'lucide-react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { Permissions, SystemRoles, roleDefaults, PermissionTypes } from 'librechat-data-provider';
|
||||
import type { Control, UseFormSetValue, UseFormGetValues } from 'react-hook-form';
|
||||
import { OGDialog, OGDialogTitle, OGDialogContent, OGDialogTrigger } from '~/components/ui';
|
||||
import {
|
||||
OGDialog,
|
||||
OGDialogTitle,
|
||||
OGDialogContent,
|
||||
OGDialogTrigger,
|
||||
Button,
|
||||
Switch,
|
||||
DropdownPopup,
|
||||
} from '~/components/ui';
|
||||
import { useUpdatePromptPermissionsMutation } from '~/data-provider';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useLocalize, useAuthContext } from '~/hooks';
|
||||
import { Button, Switch, DropdownPopup } from '~/components/ui';
|
||||
import { useToastContext } from '~/Providers';
|
||||
|
||||
type FormValues = Record<Permissions, boolean>;
|
||||
|
|
@ -18,28 +27,17 @@ type LabelControllerProps = {
|
|||
control: Control<FormValues, unknown, FormValues>;
|
||||
setValue: UseFormSetValue<FormValues>;
|
||||
getValues: UseFormGetValues<FormValues>;
|
||||
confirmChange?: (newValue: boolean, onChange: (value: boolean) => void) => void;
|
||||
};
|
||||
|
||||
const LabelController: React.FC<LabelControllerProps> = ({
|
||||
control,
|
||||
promptPerm,
|
||||
label,
|
||||
getValues,
|
||||
setValue,
|
||||
confirmChange,
|
||||
}) => (
|
||||
<div className="mb-4 flex items-center justify-between gap-2">
|
||||
<button
|
||||
className="cursor-pointer select-none"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setValue(promptPerm, !getValues(promptPerm), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
{label}
|
||||
<Controller
|
||||
name={promptPerm}
|
||||
control={control}
|
||||
|
|
@ -47,7 +45,13 @@ const LabelController: React.FC<LabelControllerProps> = ({
|
|||
<Switch
|
||||
{...field}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
onCheckedChange={(val) => {
|
||||
if (val === false && confirmChange) {
|
||||
confirmChange(val, field.onChange);
|
||||
} else {
|
||||
field.onChange(val);
|
||||
}
|
||||
}}
|
||||
value={field.value.toString()}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -59,6 +63,10 @@ const AdminSettings = () => {
|
|||
const localize = useLocalize();
|
||||
const { user, roles } = useAuthContext();
|
||||
const { showToast } = useToastContext();
|
||||
const [confirmAdminUseChange, setConfirmAdminUseChange] = useState<{
|
||||
newValue: boolean;
|
||||
callback: (value: boolean) => void;
|
||||
} | null>(null);
|
||||
const { mutate, isLoading } = useUpdatePromptPermissionsMutation({
|
||||
onSuccess: () => {
|
||||
showToast({ status: 'success', message: localize('com_ui_saved') });
|
||||
|
|
@ -137,82 +145,117 @@ const AdminSettings = () => {
|
|||
];
|
||||
|
||||
return (
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
className="h-10 w-fit gap-1 border transition-all dark:bg-transparent dark:hover:bg-surface-tertiary"
|
||||
>
|
||||
<ShieldEllipsis className="cursor-pointer" />
|
||||
<span className="hidden sm:flex">{localize('com_ui_admin')}</span>
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogContent className="w-1/4 border-border-light bg-surface-primary text-text-primary">
|
||||
<OGDialogTitle>
|
||||
{`${localize('com_ui_admin_settings')} - ${localize('com_ui_prompts')}`}
|
||||
</OGDialogTitle>
|
||||
<div className="p-2">
|
||||
{/* Role selection dropdown */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{localize('com_ui_role_select')}:</span>
|
||||
<DropdownPopup
|
||||
menuId="prompt-role-dropdown"
|
||||
isOpen={isRoleMenuOpen}
|
||||
setIsOpen={setIsRoleMenuOpen}
|
||||
trigger={
|
||||
<Ariakit.MenuButton className="inline-flex w-1/4 items-center justify-center rounded-lg border border-border-light bg-transparent px-2 py-1 text-text-primary transition-all ease-in-out hover:bg-surface-tertiary">
|
||||
{selectedRole}
|
||||
</Ariakit.MenuButton>
|
||||
}
|
||||
items={roleDropdownItems}
|
||||
itemClassName="items-center justify-center"
|
||||
sameWidth={true}
|
||||
/>
|
||||
<>
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mr-2 h-10 w-fit gap-1 border transition-all dark:bg-transparent dark:hover:bg-surface-tertiary sm:m-0"
|
||||
>
|
||||
<ShieldEllipsis className="cursor-pointer" />
|
||||
<span className="hidden sm:flex">{localize('com_ui_admin')}</span>
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogContent className="w-11/12 max-w-lg border-border-light bg-surface-primary text-text-primary">
|
||||
<OGDialogTitle>
|
||||
{`${localize('com_ui_admin_settings')} - ${localize('com_ui_prompts')}`}
|
||||
</OGDialogTitle>
|
||||
<div className="p-2">
|
||||
{/* Role selection dropdown */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{localize('com_ui_role_select')}:</span>
|
||||
<DropdownPopup
|
||||
menuId="prompt-role-dropdown"
|
||||
isOpen={isRoleMenuOpen}
|
||||
setIsOpen={setIsRoleMenuOpen}
|
||||
trigger={
|
||||
<Ariakit.MenuButton className="inline-flex w-1/5 items-center justify-center rounded-lg border border-border-light bg-transparent px-2 py-1 text-text-primary transition-all ease-in-out hover:bg-surface-tertiary">
|
||||
{selectedRole}
|
||||
</Ariakit.MenuButton>
|
||||
}
|
||||
items={roleDropdownItems}
|
||||
itemClassName="items-center justify-center"
|
||||
sameWidth={true}
|
||||
/>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="py-5">
|
||||
{labelControllerData.map(({ promptPerm, label }) => (
|
||||
<div key={promptPerm}>
|
||||
<LabelController
|
||||
control={control}
|
||||
promptPerm={promptPerm}
|
||||
label={label}
|
||||
getValues={getValues}
|
||||
setValue={setValue}
|
||||
{...(selectedRole === SystemRoles.ADMIN && promptPerm === Permissions.USE
|
||||
? {
|
||||
confirmChange: (
|
||||
newValue: boolean,
|
||||
onChange: (value: boolean) => void,
|
||||
) => setConfirmAdminUseChange({ newValue, callback: onChange }),
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
{selectedRole === SystemRoles.ADMIN && promptPerm === Permissions.USE && (
|
||||
<>
|
||||
<div className="mb-2 max-w-full whitespace-normal break-words text-sm text-red-600">
|
||||
<span>{localize('com_ui_admin_access_warning')}</span>
|
||||
{'\n'}
|
||||
<a
|
||||
href="https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/interface"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center text-blue-500 underline"
|
||||
>
|
||||
{localize('com_ui_more_info')}
|
||||
<ExternalLink size={16} className="ml-1" />
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={isSubmitting || isLoading} variant="submit">
|
||||
{localize('com_ui_save')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="py-5">
|
||||
{labelControllerData.map(({ promptPerm, label }) => (
|
||||
<div key={promptPerm}>
|
||||
<LabelController
|
||||
control={control}
|
||||
promptPerm={promptPerm}
|
||||
label={label}
|
||||
getValues={getValues}
|
||||
setValue={setValue}
|
||||
/>
|
||||
{selectedRole === SystemRoles.ADMIN && promptPerm === Permissions.USE && (
|
||||
<>
|
||||
<div className="mb-2 max-w-full whitespace-normal break-words text-sm text-red-600">
|
||||
<span>{localize('com_ui_admin_access_warning')}</span>
|
||||
{'\n'}
|
||||
<a
|
||||
href="https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/interface"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 underline"
|
||||
>
|
||||
{localize('com_ui_more_info')}
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || isLoading}
|
||||
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
|
||||
>
|
||||
{localize('com_ui_save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
|
||||
<OGDialog
|
||||
open={confirmAdminUseChange !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setConfirmAdminUseChange(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={true}
|
||||
title={localize('com_ui_confirm_change')}
|
||||
className="w-11/12 max-w-lg"
|
||||
main={<p className="mb-4">{localize('com_ui_confirm_admin_use_change')}</p>}
|
||||
selection={{
|
||||
selectHandler: () => {
|
||||
if (confirmAdminUseChange) {
|
||||
confirmAdminUseChange.callback(confirmAdminUseChange.newValue);
|
||||
}
|
||||
setConfirmAdminUseChange(null);
|
||||
},
|
||||
selectClasses:
|
||||
'bg-surface-destructive hover:bg-surface-destructive-hover text-white transition-colors duration-200',
|
||||
selectText: localize('com_ui_confirm_action'),
|
||||
isLoading: false,
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,32 +11,45 @@ const AdvancedSwitch = () => {
|
|||
const setAlwaysMakeProd = useSetRecoilState(alwaysMakeProd);
|
||||
|
||||
return (
|
||||
<div className="inline-flex h-10 items-center justify-center rounded-lg border border-border-light bg-surface-primary p-0.5 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50">
|
||||
<div className="flex flex-row items-stretch gap-0 whitespace-nowrap">
|
||||
<div className="relative flex h-10 items-center justify-center rounded-xl border border-border-light bg-surface-primary transition-all duration-300">
|
||||
<div className="relative flex w-48 items-stretch md:w-64">
|
||||
<div
|
||||
className="absolute rounded-lg bg-surface-hover shadow-lg transition-all duration-300 ease-in-out"
|
||||
style={{
|
||||
top: '1px',
|
||||
left: mode === PromptsEditorMode.SIMPLE ? '2px' : 'calc(50% + 2px)',
|
||||
width: 'calc(50% - 4px)',
|
||||
height: 'calc(100% - 2px)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Simple Mode Button */}
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors duration-200 ${
|
||||
mode === PromptsEditorMode.SIMPLE
|
||||
? 'bg-surface-tertiary text-text-primary'
|
||||
: 'bg-transparent text-text-tertiary hover:text-text-secondary'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setAlwaysMakeProd(true);
|
||||
setMode(PromptsEditorMode.SIMPLE);
|
||||
}}
|
||||
className={`relative z-10 flex-1 rounded-xl px-3 py-2 text-sm font-medium transition-all duration-300 md:px-6 ${
|
||||
mode === PromptsEditorMode.SIMPLE
|
||||
? 'text-text-primary'
|
||||
: 'text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
{localize('com_ui_simple')}
|
||||
<span className="relative">{localize('com_ui_simple')}</span>
|
||||
</button>
|
||||
|
||||
{/* Advanced Mode Button */}
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors duration-200 ${
|
||||
mode === PromptsEditorMode.ADVANCED
|
||||
? 'bg-surface-tertiary text-text-primary'
|
||||
: 'bg-transparent text-text-tertiary hover:text-text-secondary'
|
||||
}`}
|
||||
onClick={() => setMode(PromptsEditorMode.ADVANCED)}
|
||||
className={`relative z-10 flex-1 rounded-xl px-3 py-2 text-sm font-medium transition-all duration-300 md:px-6 ${
|
||||
mode === PromptsEditorMode.ADVANCED
|
||||
? 'text-text-primary'
|
||||
: 'text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
{localize('com_ui_advanced')}
|
||||
<span className="relative">{localize('com_ui_advanced')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { SquareSlash } from 'lucide-react';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Input } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const Command = ({
|
||||
|
|
@ -43,20 +44,20 @@ const Command = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border-medium">
|
||||
<h3 className="flex h-10 items-center gap-2 pl-4 text-sm text-text-secondary">
|
||||
<div className="rounded-xl border border-border-light">
|
||||
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
||||
<SquareSlash className="icon-sm" />
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
placeholder={localize('com_ui_command_placeholder')}
|
||||
value={command}
|
||||
onChange={handleInputChange}
|
||||
className="w-full rounded-lg border-none bg-surface-tertiary p-1 text-text-primary placeholder:text-text-secondary focus:bg-surface-tertiary focus:outline-none focus:ring-1 focus:ring-inset focus:ring-ring-primary md:w-96"
|
||||
className="border-none"
|
||||
/>
|
||||
{disabled !== true && (
|
||||
<span className="mr-1 w-10 text-xs text-text-secondary md:text-sm">{`${charCount}/${Constants.COMMANDS_MAX_LENGTH}`}</span>
|
||||
<span className="mr-4 w-10 text-xs text-text-secondary md:text-sm">{`${charCount}/${Constants.COMMANDS_MAX_LENGTH}`}</span>
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,15 +18,16 @@ const DeleteVersion = ({
|
|||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-10 w-10 border border-transparent bg-red-600 transition-all hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-800 p-0.5"
|
||||
aria-label="Delete version"
|
||||
className="h-10 w-10 p-0.5"
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="cursor-pointer text-white size-5" />
|
||||
<Trash2 className="size-5 cursor-pointer text-white" />
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
|
|
@ -50,7 +51,7 @@ const DeleteVersion = ({
|
|||
selection={{
|
||||
selectHandler,
|
||||
selectClasses:
|
||||
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
|
||||
'bg-surface-destructive hover:bg-surface-destructive-hover transition-colors duration-200 text-white',
|
||||
selectText: localize('com_ui_delete'),
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Info } from 'lucide-react';
|
||||
import { Input } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
const MAX_LENGTH = 56;
|
||||
const MAX_LENGTH = 120;
|
||||
|
||||
const Description = ({
|
||||
initialValue,
|
||||
|
|
@ -40,20 +41,20 @@ const Description = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border-medium">
|
||||
<h3 className="flex h-10 items-center gap-2 pl-4 text-sm text-text-secondary">
|
||||
<div className="rounded-xl border border-border-light">
|
||||
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
||||
<Info className="icon-sm" />
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
placeholder={localize('com_ui_description_placeholder')}
|
||||
value={description}
|
||||
onChange={handleInputChange}
|
||||
className="w-full rounded-lg border-none bg-surface-tertiary p-1 text-text-primary placeholder:text-text-secondary focus:bg-surface-tertiary focus:outline-none focus:ring-1 focus:ring-inset focus:ring-ring-primary md:w-96"
|
||||
className="border-none"
|
||||
/>
|
||||
{!disabled && (
|
||||
<span className="mr-1 w-10 text-xs text-text-secondary md:text-sm">{`${charCount}/${MAX_LENGTH}`}</span>
|
||||
<span className="mr-4 w-10 text-xs text-text-secondary md:text-sm">{`${charCount}/${MAX_LENGTH}`}</span>
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export default function AlwaysMakeProd({
|
|||
checked={alwaysMakeProd}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
data-testid="alwaysMakeProd"
|
||||
aria-label="Always make prompt production"
|
||||
/>
|
||||
<div>{localize('com_nav_always_make_prod')} </div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,59 +1,80 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||
import { useLocalize, useCategories } from '~/hooks';
|
||||
import { cn, createDropdownSetter } from '~/utils';
|
||||
import { SelectDropDown } from '~/components/ui';
|
||||
import { Dropdown } from '~/components/ui';
|
||||
import { useCategories } from '~/hooks';
|
||||
|
||||
const CategorySelector = ({
|
||||
currentCategory,
|
||||
onValueChange,
|
||||
className = '',
|
||||
tabIndex,
|
||||
}: {
|
||||
interface CategorySelectorProps {
|
||||
currentCategory?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
className?: string;
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
const CategorySelector: React.FC<CategorySelectorProps> = ({
|
||||
currentCategory,
|
||||
onValueChange,
|
||||
className = '',
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
const { control, watch, setValue } = useFormContext();
|
||||
const formContext = useFormContext();
|
||||
const { categories, emptyCategory } = useCategories();
|
||||
|
||||
const watchedCategory = watch('category');
|
||||
const control = formContext.control;
|
||||
const watch = formContext.watch;
|
||||
const setValue = formContext.setValue;
|
||||
|
||||
const watchedCategory = watch ? watch('category') : currentCategory;
|
||||
|
||||
const categoryOption = useMemo(
|
||||
() =>
|
||||
categories.find((category) => category.value === (watchedCategory ?? currentCategory)) ??
|
||||
emptyCategory,
|
||||
(categories ?? []).find(
|
||||
(category) => category.value === (watchedCategory ?? currentCategory),
|
||||
) ?? emptyCategory,
|
||||
[watchedCategory, categories, currentCategory, emptyCategory],
|
||||
);
|
||||
|
||||
return (
|
||||
return formContext ? (
|
||||
<Controller
|
||||
name="category"
|
||||
control={control}
|
||||
render={() => (
|
||||
<SelectDropDown
|
||||
title="Category"
|
||||
tabIndex={tabIndex}
|
||||
value={categoryOption || ''}
|
||||
setValue={createDropdownSetter((value: string) => {
|
||||
<Dropdown
|
||||
value={categoryOption.value ?? ''}
|
||||
onChange={(value: string) => {
|
||||
setValue('category', value, { shouldDirty: false });
|
||||
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
||||
onValueChange?.(value);
|
||||
})}
|
||||
availableValues={categories}
|
||||
showAbove={false}
|
||||
showLabel={false}
|
||||
emptyTitle={true}
|
||||
showOptionIcon={true}
|
||||
searchPlaceholder={localize('com_ui_search_categories')}
|
||||
className={cn('h-10 w-56 cursor-pointer', className)}
|
||||
currentValueClass="text-md gap-2"
|
||||
optionsListClass="text-sm max-h-72"
|
||||
}}
|
||||
aria-labelledby="category-selector-label"
|
||||
ariaLabel="Prompt's category selector"
|
||||
className={className}
|
||||
options={categories || []}
|
||||
renderValue={(option) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
{option.icon != null && <span>{option.icon as React.ReactNode}</span>}
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Dropdown
|
||||
value={currentCategory ?? ''}
|
||||
onChange={(value: string) => {
|
||||
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
||||
onValueChange?.(value);
|
||||
}}
|
||||
aria-labelledby="category-selector-label"
|
||||
ariaLabel="Prompt's category selector"
|
||||
className={className}
|
||||
options={categories || []}
|
||||
renderValue={(option) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
{option.icon != null && <span>{option.icon as React.ReactNode}</span>}
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ function ChatGroupItem({
|
|||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
className="z-50 inline-flex h-7 w-7 items-center justify-center rounded-md border border-border-medium bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out hover:border-border-heavy hover:bg-surface-secondary focus:border-border-heavy focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||
className="z-50 inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border-medium bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out hover:border-border-heavy hover:bg-surface-hover focus:border-border-heavy focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<MenuIcon className="icon-md text-text-secondary" aria-hidden="true" />
|
||||
<span className="sr-only">Open actions menu for {group.name}</span>
|
||||
|
|
@ -89,7 +89,7 @@ function ChatGroupItem({
|
|||
<DropdownMenuContent
|
||||
id={`prompt-menu-${group._id}`}
|
||||
aria-label={`Available actions for ${group.name}`}
|
||||
className="z-50 mt-2 w-36 rounded-lg"
|
||||
className="z-50 w-fit rounded-xl"
|
||||
collisionPadding={2}
|
||||
align="end"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ const CreatePromptForm = ({
|
|||
</div>
|
||||
)}
|
||||
/>
|
||||
<CategorySelector tabIndex={0} />
|
||||
<CategorySelector />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-4 md:mt-[1.075rem]">
|
||||
|
|
@ -166,7 +166,12 @@ const CreatePromptForm = ({
|
|||
/>
|
||||
<Command onValueChange={(value) => methods.setValue('command', value)} tabIndex={0} />
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button tabIndex={0} type="submit" disabled={!isDirty || isSubmitting || !isValid}>
|
||||
<Button
|
||||
aria-label={localize('com_ui_create_prompt')}
|
||||
tabIndex={0}
|
||||
type="submit"
|
||||
disabled={!isDirty || isSubmitting || !isValid}
|
||||
>
|
||||
{localize('com_ui_create_prompt')}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,237 +1,175 @@
|
|||
import { useState, useRef, useMemo } from 'react';
|
||||
import { MenuIcon, EarthIcon } from 'lucide-react';
|
||||
import { memo, useState, useRef, useMemo, useCallback, KeyboardEvent } from 'react';
|
||||
import { EarthIcon, Pen } from 'lucide-react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { SystemRoles, type TPromptGroup } from 'librechat-data-provider';
|
||||
import { useDeletePromptGroup, useUpdatePromptGroup } from '~/data-provider';
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
Button,
|
||||
OGDialog,
|
||||
DropdownMenu,
|
||||
OGDialogTrigger,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuItem,
|
||||
} from '~/components/ui';
|
||||
import { Input, Label, Button, OGDialog, OGDialogTrigger } from '~/components/ui';
|
||||
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useLocalize, useAuthContext } from '~/hooks';
|
||||
import { TrashIcon } from '~/components/svg';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
export default function DashGroupItem({
|
||||
group,
|
||||
instanceProjectId,
|
||||
}: {
|
||||
interface DashGroupItemProps {
|
||||
group: TPromptGroup;
|
||||
instanceProjectId?: string;
|
||||
}) {
|
||||
}
|
||||
|
||||
function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps) {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
|
||||
const { user } = useAuthContext();
|
||||
const blurTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const [nameEditFlag, setNameEditFlag] = useState(false);
|
||||
const [nameInputField, setNameInputField] = useState(group.name);
|
||||
const isOwner = useMemo(() => user?.id === group.author, [user, group]);
|
||||
const groupIsGlobal = useMemo(
|
||||
() => instanceProjectId != null && group.projectIds?.includes(instanceProjectId),
|
||||
[group, instanceProjectId],
|
||||
|
||||
const blurTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [nameInputValue, setNameInputValue] = useState(group.name);
|
||||
|
||||
const isOwner = useMemo(() => user?.id === group.author, [user?.id, group.author]);
|
||||
const isGlobalGroup = useMemo(
|
||||
() => instanceProjectId && group.projectIds?.includes(instanceProjectId),
|
||||
[group.projectIds, instanceProjectId],
|
||||
);
|
||||
|
||||
const updateGroup = useUpdatePromptGroup({
|
||||
onMutate: () => {
|
||||
clearTimeout(blurTimeoutRef.current);
|
||||
setNameEditFlag(false);
|
||||
if (blurTimeoutRef.current) {
|
||||
clearTimeout(blurTimeoutRef.current);
|
||||
}
|
||||
},
|
||||
});
|
||||
const deletePromptGroupMutation = useDeletePromptGroup({
|
||||
onSuccess: (response, variables) => {
|
||||
|
||||
const deleteGroup = useDeletePromptGroup({
|
||||
onSuccess: (_response, variables) => {
|
||||
if (variables.id === group._id) {
|
||||
navigate('/d/prompts');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const cancelRename = () => {
|
||||
setNameEditFlag(false);
|
||||
};
|
||||
const { isLoading } = updateGroup;
|
||||
|
||||
const saveRename = () => {
|
||||
updateGroup.mutate({ payload: { name: nameInputField }, id: group._id ?? '' });
|
||||
};
|
||||
const handleSaveRename = useCallback(() => {
|
||||
console.log(group._id ?? '', { name: nameInputValue });
|
||||
updateGroup.mutate({ id: group._id ?? '', payload: { name: nameInputValue } });
|
||||
}, [group._id, nameInputValue, updateGroup]);
|
||||
|
||||
const handleBlur = () => {
|
||||
blurTimeoutRef.current = setTimeout(() => {
|
||||
cancelRename();
|
||||
}, 100);
|
||||
};
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
navigate(`/d/prompts/${group._id}`, { replace: true });
|
||||
}
|
||||
},
|
||||
[group._id, navigate],
|
||||
);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
navigate(`/d/prompts/${group._id}`, { replace: true });
|
||||
}
|
||||
};
|
||||
const triggerDelete = useCallback(() => {
|
||||
deleteGroup.mutate({ id: group._id ?? '' });
|
||||
}, [group._id, deleteGroup]);
|
||||
|
||||
const handleRename = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
setNameEditFlag(true);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
deletePromptGroupMutation.mutate({ id: group._id ?? '' });
|
||||
};
|
||||
const handleContainerClick = useCallback(() => {
|
||||
navigate(`/d/prompts/${group._id}`, { replace: true });
|
||||
}, [group._id, navigate]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-100 mx-2 my-3 flex cursor-pointer flex-row rounded-md border-0 bg-white p-4 transition-all duration-300 ease-in-out hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600',
|
||||
params.promptId === group._id && 'bg-gray-100/50 dark:bg-gray-600 ',
|
||||
'mx-2 my-2 flex cursor-pointer rounded-lg border border-border-light bg-surface-primary p-3 shadow-sm transition-all duration-300 ease-in-out hover:bg-surface-secondary',
|
||||
params.promptId === group._id && 'bg-surface-hover',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!nameEditFlag) {
|
||||
navigate(`/d/prompts/${group._id}`, { replace: true });
|
||||
}
|
||||
}}
|
||||
onClick={handleContainerClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`${group.name} prompt group`}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-start truncate">
|
||||
<div className="relative flex w-full cursor-pointer flex-col gap-1 text-start align-top">
|
||||
{nameEditFlag ? (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-2 truncate pr-2">
|
||||
<CategoryIcon category={group.category ?? ''} className="icon-lg" aria-hidden="true" />
|
||||
|
||||
<Label className="text-md cursor-pointer truncate font-semibold text-text-primary">
|
||||
{group.name}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full items-center gap-2">
|
||||
{isGlobalGroup && (
|
||||
<EarthIcon
|
||||
className="icon-md text-green-500"
|
||||
aria-label={localize('com_ui_global_group')}
|
||||
/>
|
||||
)}
|
||||
{(isOwner || user?.role === SystemRoles.ADMIN) && (
|
||||
<>
|
||||
<div className="flex w-full gap-2">
|
||||
<Label htmlFor="group-name-input" className="sr-only">
|
||||
{localize('com_ui_rename_group')}
|
||||
</Label>
|
||||
<Input
|
||||
id="group-name-input"
|
||||
value={nameInputField}
|
||||
tabIndex={0}
|
||||
className="w-full"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setNameInputField(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
cancelRename();
|
||||
} else if (e.key === 'Enter') {
|
||||
saveRename();
|
||||
}
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
aria-label={localize('com_ui_rename_group')}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
saveRename();
|
||||
}}
|
||||
aria-label={localize('com_ui_save')}
|
||||
>
|
||||
{localize('com_ui_save')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="break-word line-clamp-3 text-balance text-sm text-gray-600 dark:text-gray-400">
|
||||
{localize('com_ui_renaming_var', group.name)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="flex flex-row gap-2">
|
||||
<CategoryIcon
|
||||
category={group.category ?? ''}
|
||||
className="icon-md"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h3 className="break-word text-balance text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
{group.name}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{groupIsGlobal === true && (
|
||||
<EarthIcon
|
||||
className="icon-md text-green-400"
|
||||
aria-label={localize('com_ui_global_group')}
|
||||
/>
|
||||
)}
|
||||
{(isOwner || user?.role === SystemRoles.ADMIN) && (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-7 w-7 p-0 hover:bg-gray-200 dark:bg-gray-800/50 dark:text-gray-400 dark:hover:border-gray-400 dark:focus:border-gray-500"
|
||||
aria-label={localize('com_ui_more_options')}
|
||||
>
|
||||
<MenuIcon className="icon-md dark:text-gray-300" aria-hidden="true" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="mt-2 w-36 rounded-lg" collisionPadding={2}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={handleRename}>
|
||||
{localize('com_ui_rename')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'h-7 w-7 p-0 hover:bg-gray-200 dark:bg-gray-800/50 dark:text-gray-400 dark:hover:border-gray-400 dark:focus:border-gray-500',
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={localize('com_ui_delete_prompt')}
|
||||
>
|
||||
<TrashIcon
|
||||
className="icon-md text-gray-600 dark:text-gray-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_delete_prompt')}
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<>
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="confirm-delete"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{localize('com_ui_delete_confirm')}{' '}
|
||||
<strong>{group.name}</strong>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: handleDelete,
|
||||
selectClasses:
|
||||
'bg-red-600 dark:bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
|
||||
selectText: localize('com_ui_delete'),
|
||||
}}
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
||||
aria-label={localize('com_ui_rename_prompt') + ' ' + group.name}
|
||||
>
|
||||
<Pen className="icon-sm text-text-primary" aria-hidden="true" />
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_rename_prompt')}
|
||||
className="w-11/12 max-w-lg"
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Input
|
||||
value={nameInputValue}
|
||||
onChange={(e) => setNameInputValue(e.target.value)}
|
||||
className="w-full"
|
||||
aria-label={localize('com_ui_rename_prompt') + ' ' + group.name}
|
||||
/>
|
||||
</OGDialog>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ellipsis text-balance text-sm text-gray-600 dark:text-gray-400">
|
||||
{group.oneliner ?? '' ? group.oneliner : group.productionPrompt?.prompt ?? ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: handleSaveRename,
|
||||
selectClasses:
|
||||
'bg-surface-submit hover:bg-surface-submit-hover text-white disabled:hover:bg-surface-submit',
|
||||
selectText: localize('com_ui_save'),
|
||||
isLoading,
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={localize('com_ui_delete_prompt') + ' ' + group.name}
|
||||
>
|
||||
<TrashIcon className="icon-sm text-text-primary" aria-hidden="true" />
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_delete_prompt')}
|
||||
className="w-11/12 max-w-lg"
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="confirm-delete" className="text-left text-sm font-medium">
|
||||
{localize('com_ui_delete_confirm')} <strong>{group.name}</strong>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: triggerDelete,
|
||||
selectClasses:
|
||||
'bg-red-600 dark:bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
|
||||
selectText: localize('com_ui_delete'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -239,3 +177,5 @@ export default function DashGroupItem({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DashGroupItemComponent);
|
||||
|
|
|
|||
|
|
@ -1,109 +1,13 @@
|
|||
import { ListFilter, User, Share2, Dot } from 'lucide-react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { ListFilter, User, Share2 } from 'lucide-react';
|
||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { SystemCategories } from 'librechat-data-provider';
|
||||
import type { OptionWithIcon } from '~/common';
|
||||
import { usePromptGroupsNav, useLocalize, useCategories } from '~/hooks';
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuSeparator,
|
||||
} from '~/components/ui';
|
||||
import { Dropdown, AnimatedSearchInput } from '~/components/ui';
|
||||
import type { Option } from '~/common';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
export function FilterItem({
|
||||
label,
|
||||
icon,
|
||||
onClick,
|
||||
isActive,
|
||||
}: {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={onClick}
|
||||
className="relative cursor-pointer gap-2 text-text-secondary hover:bg-surface-tertiary focus:bg-surface-tertiary"
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
{isActive === true && (
|
||||
<span className="absolute bottom-0 right-0 top-0 flex items-center">
|
||||
<Dot />
|
||||
</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterMenu({
|
||||
onSelect,
|
||||
}: {
|
||||
onSelect: (category: string, icon?: React.ReactNode | null) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { categories } = useCategories('h-4 w-4');
|
||||
const memoizedCategories = useMemo(() => {
|
||||
const noCategory = {
|
||||
label: localize('com_ui_no_category'),
|
||||
value: SystemCategories.NO_CATEGORY,
|
||||
};
|
||||
if (!categories) {
|
||||
return [noCategory];
|
||||
}
|
||||
|
||||
return [noCategory, ...categories];
|
||||
}, [categories, localize]);
|
||||
|
||||
const categoryFilter = useRecoilValue(store.promptsCategory);
|
||||
return (
|
||||
<DropdownMenuContent className="max-h-xl min-w-48 overflow-y-auto">
|
||||
<DropdownMenuGroup>
|
||||
<FilterItem
|
||||
label={localize('com_ui_all_proper')}
|
||||
icon={<ListFilter className="h-4 w-4 text-text-primary" />}
|
||||
onClick={() => onSelect(SystemCategories.ALL, <ListFilter className="icon-sm" />)}
|
||||
isActive={categoryFilter === ''}
|
||||
/>
|
||||
<FilterItem
|
||||
label={localize('com_ui_my_prompts')}
|
||||
icon={<User className="h-4 w-4 text-text-primary" />}
|
||||
onClick={() => onSelect(SystemCategories.MY_PROMPTS, <User className="h-4 w-4" />)}
|
||||
isActive={categoryFilter === SystemCategories.MY_PROMPTS}
|
||||
/>
|
||||
<FilterItem
|
||||
label={localize('com_ui_shared_prompts')}
|
||||
icon={<Share2 className="h-4 w-4 text-text-primary" />}
|
||||
onClick={() => onSelect(SystemCategories.SHARED_PROMPTS, <Share2 className="h-4 w-4" />)}
|
||||
isActive={categoryFilter === SystemCategories.SHARED_PROMPTS}
|
||||
/>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
{memoizedCategories
|
||||
.filter((category) => category.value)
|
||||
.map((category, i) => (
|
||||
<FilterItem
|
||||
key={`${category.value}-${i}`}
|
||||
label={category.label}
|
||||
icon={(category as OptionWithIcon).icon}
|
||||
onClick={() => onSelect(category.value, (category as OptionWithIcon).icon)}
|
||||
isActive={category.value === categoryFilter}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FilterPrompts({
|
||||
setName,
|
||||
className = '',
|
||||
|
|
@ -113,46 +17,81 @@ export default function FilterPrompts({
|
|||
const localize = useLocalize();
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const setCategory = useSetRecoilState(store.promptsCategory);
|
||||
const [selectedIcon, setSelectedIcon] = useState(<ListFilter className="icon-sm" />);
|
||||
const categoryFilter = useRecoilValue(store.promptsCategory);
|
||||
const { categories } = useCategories('h-4 w-4');
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
const filterOptions = useMemo(() => {
|
||||
const baseOptions: Option[] = [
|
||||
{
|
||||
value: SystemCategories.ALL,
|
||||
label: localize('com_ui_all_proper'),
|
||||
icon: <ListFilter className="h-4 w-4 text-text-primary" />,
|
||||
},
|
||||
{
|
||||
value: SystemCategories.MY_PROMPTS,
|
||||
label: localize('com_ui_my_prompts'),
|
||||
icon: <User className="h-4 w-4 text-text-primary" />,
|
||||
},
|
||||
{
|
||||
value: SystemCategories.SHARED_PROMPTS,
|
||||
label: localize('com_ui_shared_prompts'),
|
||||
icon: <Share2 className="h-4 w-4 text-text-primary" />,
|
||||
},
|
||||
{ divider: true, value: null },
|
||||
];
|
||||
|
||||
const categoryOptions = categories
|
||||
? [...categories]
|
||||
: [
|
||||
{
|
||||
value: SystemCategories.NO_CATEGORY,
|
||||
label: localize('com_ui_no_category'),
|
||||
},
|
||||
];
|
||||
|
||||
return [...baseOptions, ...categoryOptions];
|
||||
}, [categories, localize]);
|
||||
|
||||
const onSelect = useCallback(
|
||||
(category: string, icon?: React.ReactNode | null) => {
|
||||
if (category === SystemCategories.ALL) {
|
||||
setSelectedIcon(<ListFilter className="icon-sm" />);
|
||||
return setCategory('');
|
||||
}
|
||||
setCategory(category);
|
||||
if (icon != null && React.isValidElement(icon)) {
|
||||
setSelectedIcon(icon);
|
||||
(value: string) => {
|
||||
if (value === SystemCategories.ALL) {
|
||||
setCategory('');
|
||||
} else {
|
||||
setCategory(value);
|
||||
}
|
||||
},
|
||||
[setCategory],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsSearching(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setIsSearching(false);
|
||||
}, 500);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [displayName]);
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-2 text-text-primary', className)}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-10 w-10 flex-shrink-0"
|
||||
id="filter-prompts"
|
||||
aria-label="filter-prompts"
|
||||
>
|
||||
{selectedIcon}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<FilterMenu onSelect={onSelect} />
|
||||
</DropdownMenu>
|
||||
<Input
|
||||
placeholder={localize('com_ui_filter_prompts_name')}
|
||||
<div className={cn('flex w-full gap-2 text-text-primary', className)}>
|
||||
<Dropdown
|
||||
value={categoryFilter || SystemCategories.ALL}
|
||||
onChange={onSelect}
|
||||
options={filterOptions}
|
||||
className="bg-transparent"
|
||||
icon={<ListFilter className="h-4 w-4" />}
|
||||
label="Filter: "
|
||||
ariaLabel={localize('com_ui_filter_prompts')}
|
||||
iconOnly
|
||||
/>
|
||||
<AnimatedSearchInput
|
||||
value={displayName}
|
||||
onChange={(e) => {
|
||||
setDisplayName(e.target.value);
|
||||
setName(e.target.value);
|
||||
}}
|
||||
className="w-full border-border-light placeholder:text-text-secondary"
|
||||
isSearching={isSearching}
|
||||
placeholder={localize('com_ui_filter_prompts_name')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function GroupSidePanel({
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex w-full min-w-72 flex-col gap-2 overflow-y-auto md:w-full lg:w-1/4 xl:w-1/4',
|
||||
'mr-2 flex h-auto w-auto min-w-72 flex-col gap-2 lg:w-1/4 xl:w-1/4',
|
||||
isDetailView && isSmallerScreen ? 'hidden' : '',
|
||||
className,
|
||||
)}
|
||||
|
|
@ -39,7 +39,7 @@ export default function GroupSidePanel({
|
|||
<List
|
||||
groups={promptGroups}
|
||||
isChatRoute={isChatRoute}
|
||||
isLoading={!!groupsQuery?.isLoading}
|
||||
isLoading={!!groupsQuery.isLoading}
|
||||
/>
|
||||
</div>
|
||||
<PanelNavigation
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Plus } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { TPromptGroup, TStartupConfig } from 'librechat-data-provider';
|
||||
|
|
@ -31,10 +32,11 @@ export default function List({
|
|||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mx-2 w-full bg-transparent px-3"
|
||||
className="w-full bg-transparent px-3"
|
||||
onClick={() => navigate('/d/prompts/new')}
|
||||
>
|
||||
+ {localize('com_ui_create_prompt')}
|
||||
<Plus className="size-4" aria-hidden />
|
||||
{localize('com_ui_create_prompt')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -43,16 +45,18 @@ export default function List({
|
|||
{isLoading && isChatRoute && (
|
||||
<Skeleton className="my-2 flex h-[84px] w-full rounded-2xl border-0 px-3 pb-4 pt-3" />
|
||||
)}
|
||||
{isLoading && !isChatRoute && (
|
||||
<Skeleton className="w-100 mx-2 my-3 flex h-[72px] rounded-md border-0 p-4" />
|
||||
)}
|
||||
{isLoading &&
|
||||
!isChatRoute &&
|
||||
Array.from({ length: 10 }).map((_, index: number) => (
|
||||
<Skeleton key={index} className="w-100 mx-2 my-2 flex h-14 rounded-lg border-0 p-4" />
|
||||
))}
|
||||
{!isLoading && groups.length === 0 && isChatRoute && (
|
||||
<div className="my-2 flex h-[84px] w-full items-center justify-center rounded-2xl border border-border-light bg-transparent px-3 pb-4 pt-3 text-text-primary">
|
||||
{localize('com_ui_nothing_found')}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && groups.length === 0 && !isChatRoute && (
|
||||
<div className="w-100 mx-2 my-3 flex h-[72px] items-center justify-center rounded-md border border-border-light bg-transparent p-4 text-text-primary">
|
||||
<div className="my-12 flex w-full items-center justify-center text-lg font-semibold text-text-primary">
|
||||
{localize('com_ui_nothing_found')}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
||||
import { Label } from '~/components/ui';
|
||||
|
||||
export default function ListCard({
|
||||
category,
|
||||
|
|
@ -25,25 +26,31 @@ export default function ListCard({
|
|||
<div
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="relative my-2 flex w-full cursor-pointer flex-col gap-2 rounded-2xl border border-border-light px-3 pb-4 pt-3 text-start
|
||||
align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-all duration-300 ease-in-out hover:bg-surface-tertiary"
|
||||
className="relative my-2 flex w-full cursor-pointer flex-col gap-2 rounded-xl border border-border-light px-3 pb-4 pt-3 text-start
|
||||
align-top text-[15px] shadow-sm transition-all duration-300 ease-in-out hover:bg-surface-tertiary hover:shadow-lg"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-labelledby={`card-title-${name}`}
|
||||
aria-describedby={`card-snippet-${name}`}
|
||||
aria-label={`Card for ${name}`}
|
||||
>
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="flex w-full justify-between gap-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
<CategoryIcon category={category} className="icon-md" aria-hidden="true" />
|
||||
<h3
|
||||
<Label
|
||||
id={`card-title-${name}`}
|
||||
className="break-word select-none text-balance text-sm font-semibold text-text-primary"
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</h3>
|
||||
</Label>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
<div className="ellipsis max-w-full select-none text-balance text-sm text-text-secondary">
|
||||
<div
|
||||
id={`card-snippet-${name}`}
|
||||
className="ellipsis max-w-full select-none text-balance text-sm text-text-secondary"
|
||||
>
|
||||
{snippet}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ function PanelNavigation({
|
|||
}) {
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<div className="my-1 flex justify-between px-4">
|
||||
<div className="my-1 flex justify-between">
|
||||
<div className="mb-2 flex gap-2">
|
||||
{!isChatRoute && <ThemeSelector returnThemeOnly={true} />}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import {
|
|||
extractVariableInfo,
|
||||
} from '~/utils';
|
||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { TextareaAutosize, InputCombobox, Button } from '~/components/ui';
|
||||
import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks';
|
||||
import { TextareaAutosize, InputCombobox } from '~/components/ui';
|
||||
|
||||
type FieldType = 'text' | 'select';
|
||||
|
||||
|
|
@ -202,12 +202,9 @@ export default function VariableForm({
|
|||
))}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn rounded bg-green-500 px-4 py-2 font-bold text-white transition-all hover:bg-green-600"
|
||||
>
|
||||
<Button type="submit" variant="submit">
|
||||
{localize('com_ui_submit')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,10 +14,19 @@ export default function ManagePrompts({ className }: { className?: string }) {
|
|||
setPromptsCategory('');
|
||||
}, [setPromptsName, setPromptsCategory]);
|
||||
|
||||
const clickHandler = useCustomLink('/d/prompts', clickCallback);
|
||||
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}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(className, 'bg-transparent')}
|
||||
onClick={clickHandler}
|
||||
aria-label="Manage Prompts"
|
||||
role="button"
|
||||
>
|
||||
{localize('com_ui_manage')}
|
||||
</Button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const PreviewPrompt = ({
|
|||
}) => {
|
||||
return (
|
||||
<OGDialog open={open} onOpenChange={onOpenChange}>
|
||||
<OGDialogContent className="max-w-full bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300 md:max-w-3xl">
|
||||
<OGDialogContent className="w-11/12 max-w-5xl">
|
||||
<div className="p-2">
|
||||
<PromptDetails group={group} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import CategoryIcon from './Groups/CategoryIcon';
|
|||
import PromptVariables from './PromptVariables';
|
||||
import { PromptVariableGfm } from './Markdown';
|
||||
import { replaceSpecialVars } from '~/utils';
|
||||
import { Label } from '~/components/ui';
|
||||
import Description from './Description';
|
||||
import Command from './Command';
|
||||
|
||||
|
|
@ -30,25 +31,25 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col items-center justify-between px-4 dark:text-gray-200 sm:flex-row">
|
||||
<div className="flex flex-col items-center justify-between p-4 text-text-primary sm:flex-row">
|
||||
<div className="mb-1 flex flex-row items-center font-bold sm:text-xl md:mb-0 md:text-2xl">
|
||||
<div className="mb-1 flex items-center md:mb-0">
|
||||
<div className="rounded p-2">
|
||||
<div className="rounded pr-2">
|
||||
{(group.category?.length ?? 0) > 0 ? (
|
||||
<CategoryIcon category={group.category ?? ''} />
|
||||
) : null}
|
||||
</div>
|
||||
<span className="mr-2 border border-transparent p-2">{group.name}</span>
|
||||
<Label className="text-2xl font-bold">{group.name}</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full max-h-screen w-full max-w-[90vw] flex-col overflow-y-auto md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-4 border-gray-300 p-0 dark:border-gray-600 md:max-h-[calc(100vh-150px)] md:p-2">
|
||||
<div className="flex h-full max-h-screen flex-col overflow-y-auto md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-4 p-0 md:max-h-[calc(100vh-150px)] md:p-2">
|
||||
<div>
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-gray-300 py-2 pl-4 text-base font-semibold dark:border-gray-600 dark:text-gray-200">
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-light py-2 pl-4 text-base font-semibold text-text-primary ">
|
||||
{localize('com_ui_prompt_text')}
|
||||
</h2>
|
||||
<div className="group relative min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 dark:border-gray-600 sm:max-w-full">
|
||||
<div className="group relative min-h-32 rounded-b-lg border border-border-light p-4 transition-all duration-150">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[
|
||||
/** @ts-ignore */
|
||||
|
|
|
|||
|
|
@ -54,17 +54,24 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-medium py-2 pl-4 text-base font-semibold text-text-primary">
|
||||
{localize('com_ui_prompt_text')}
|
||||
<div className="flex flex-row gap-6">
|
||||
<div className="flex max-h-[85vh] flex-col sm:max-h-[85vh]">
|
||||
<h2 className="flex items-center justify-between rounded-t-xl border border-border-light py-1.5 pl-3 text-sm font-semibold text-text-primary sm:py-2 sm:pl-4 sm:text-base">
|
||||
<span className="max-w-[200px] truncate sm:max-w-none">
|
||||
{localize('com_ui_prompt_text')}
|
||||
</span>
|
||||
<div className="flex flex-shrink-0 flex-row gap-3 sm:gap-6">
|
||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||
<AlwaysMakeProd className="hidden sm:flex" />
|
||||
)}
|
||||
<button type="button" onClick={() => setIsEditing((prev) => !prev)} className="mr-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditing((prev) => !prev)}
|
||||
aria-label={isEditing ? localize('com_ui_save') : localize('com_ui_edit')}
|
||||
className="mr-1 rounded-lg p-1.5 sm:mr-2 sm:p-1"
|
||||
>
|
||||
<EditorIcon
|
||||
className={cn(
|
||||
'icon-lg',
|
||||
'h-5 w-5 sm:h-6 sm:w-6',
|
||||
isEditing ? 'p-[0.05rem]' : 'text-secondary-alt hover:text-text-primary',
|
||||
)}
|
||||
/>
|
||||
|
|
@ -74,8 +81,11 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
<div
|
||||
role="button"
|
||||
className={cn(
|
||||
'min-h-[8rem] w-full rounded-b-lg border border-border-medium p-4 transition-all duration-150',
|
||||
{ 'cursor-pointer bg-surface-secondary-alt hover:bg-surface-tertiary': !isEditing },
|
||||
'w-full flex-1 overflow-auto rounded-b-xl border border-border-light p-2 transition-all duration-150 sm:p-4',
|
||||
{
|
||||
'cursor-pointer bg-surface-primary hover:bg-surface-secondary active:bg-surface-tertiary':
|
||||
!isEditing,
|
||||
},
|
||||
)}
|
||||
onClick={() => !isEditing && setIsEditing(true)}
|
||||
onKeyDown={(e) => {
|
||||
|
|
@ -86,7 +96,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
tabIndex={0}
|
||||
>
|
||||
{!isEditing && (
|
||||
<EditIcon className="icon-xl absolute inset-0 m-auto hidden text-text-primary opacity-25 group-hover:block" />
|
||||
<EditIcon className="icon-xl absolute inset-0 m-auto hidden h-6 w-6 text-text-primary opacity-25 group-hover:block sm:h-8 sm:w-8" />
|
||||
)}
|
||||
<Controller
|
||||
name={name}
|
||||
|
|
@ -95,8 +105,9 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
isEditing ? (
|
||||
<TextareaAutosize
|
||||
{...field}
|
||||
className="w-full rounded border border-border-medium bg-transparent px-2 py-1 text-text-primary focus:outline-none"
|
||||
className="w-full resize-none overflow-y-auto rounded bg-transparent text-sm text-text-primary focus:outline-none sm:text-base"
|
||||
minRows={3}
|
||||
maxRows={14}
|
||||
onBlur={() => setIsEditing(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
|
|
@ -106,17 +117,26 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
}}
|
||||
/>
|
||||
) : (
|
||||
<ReactMarkdown
|
||||
/** @ts-ignore */
|
||||
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
||||
/** @ts-ignore */
|
||||
rehypePlugins={rehypePlugins}
|
||||
/** @ts-ignore */
|
||||
components={{ p: PromptVariableGfm, code: codeNoExecution }}
|
||||
className="markdown prose dark:prose-invert light my-1 w-full break-words text-text-primary"
|
||||
<div
|
||||
className={cn('overflow-y-auto text-sm sm:text-base')}
|
||||
style={{ minHeight: '4.5em', maxHeight: '21em', overflow: 'auto' }}
|
||||
>
|
||||
{field.value}
|
||||
</ReactMarkdown>
|
||||
<ReactMarkdown
|
||||
/** @ts-ignore */
|
||||
remarkPlugins={[
|
||||
supersub,
|
||||
remarkGfm,
|
||||
[remarkMath, { singleDollarTextMath: true }],
|
||||
]}
|
||||
/** @ts-ignore */
|
||||
rehypePlugins={rehypePlugins}
|
||||
/** @ts-ignore */
|
||||
components={{ p: PromptVariableGfm, code: codeNoExecution }}
|
||||
className="markdown prose dark:prose-invert light my-1 w-full break-words text-text-primary"
|
||||
>
|
||||
{field.value}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
import { Rocket } from 'lucide-react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Menu, Rocket } from 'lucide-react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { useParams, useOutletContext } from 'react-router-dom';
|
||||
import { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { useNavigate, useParams, useOutletContext } from 'react-router-dom';
|
||||
import { PermissionTypes, Permissions, SystemRoles } from 'librechat-data-provider';
|
||||
import type { TCreatePrompt, TPrompt } from 'librechat-data-provider';
|
||||
import type { TCreatePrompt } from 'librechat-data-provider';
|
||||
import { SystemRoles, PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import {
|
||||
useGetPrompts,
|
||||
useCreatePrompt,
|
||||
useDeletePrompt,
|
||||
useGetPrompts,
|
||||
useGetPromptGroup,
|
||||
useUpdatePromptGroup,
|
||||
useMakePromptProduction,
|
||||
useDeletePrompt,
|
||||
} from '~/data-provider';
|
||||
import { useAuthContext, usePromptGroupsNav, useHasAccess, useLocalize } from '~/hooks';
|
||||
import CategorySelector from './Groups/CategorySelector';
|
||||
import AlwaysMakeProd from './Groups/AlwaysMakeProd';
|
||||
import NoPromptGroup from './Groups/NoPromptGroup';
|
||||
import { Button, Skeleton } from '~/components/ui';
|
||||
import PromptVariables from './PromptVariables';
|
||||
import { cn, findPromptGroup } from '~/utils';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import PromptVersions from './PromptVersions';
|
||||
import { PromptsEditorMode } from '~/common';
|
||||
import DeleteConfirm from './DeleteVersion';
|
||||
import PromptDetails from './PromptDetails';
|
||||
import { findPromptGroup } from '~/utils';
|
||||
import PromptEditor from './PromptEditor';
|
||||
import SkeletonForm from './SkeletonForm';
|
||||
import Description from './Description';
|
||||
|
|
@ -34,82 +33,52 @@ import PromptName from './PromptName';
|
|||
import Command from './Command';
|
||||
import store from '~/store';
|
||||
|
||||
const { promptsEditorMode } = store;
|
||||
|
||||
const PromptForm = () => {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
|
||||
const { user } = useAuthContext();
|
||||
const { showToast } = useToastContext();
|
||||
const editorMode = useRecoilValue(promptsEditorMode);
|
||||
const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd);
|
||||
const { data: group, isLoading: isLoadingGroup } = useGetPromptGroup(params.promptId || '');
|
||||
const { data: prompts = [], isLoading: isLoadingPrompts } = useGetPrompts(
|
||||
{ groupId: params.promptId ?? '' },
|
||||
{ enabled: !!params.promptId },
|
||||
);
|
||||
const { showToast } = useToastContext();
|
||||
const promptId = params.promptId || '';
|
||||
|
||||
const [selectionIndex, setSelectionIndex] = useState<number>(0);
|
||||
const editorMode = useRecoilValue(store.promptsEditorMode);
|
||||
const prevIsEditingRef = useRef(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [initialLoad, setInitialLoad] = useState(true);
|
||||
const [selectionIndex, setSelectionIndex] = useState<number>(0);
|
||||
const isOwner = useMemo(() => user?.id === group?.author, [user, group]);
|
||||
const selectedPrompt = useMemo(
|
||||
() => prompts[selectionIndex] as TPrompt | undefined,
|
||||
[prompts, selectionIndex],
|
||||
const [showSidePanel, setShowSidePanel] = useState(false);
|
||||
const sidePanelWidth = '320px';
|
||||
|
||||
// Fetch group early so it is available for later hooks.
|
||||
const { data: group, isLoading: isLoadingGroup } = useGetPromptGroup(promptId);
|
||||
const { data: prompts = [], isLoading: isLoadingPrompts } = useGetPrompts(
|
||||
{ groupId: promptId },
|
||||
{ enabled: !!promptId },
|
||||
);
|
||||
|
||||
const isOwner = useMemo(() => (user && group ? user.id === group.author : false), [user, group]);
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: {
|
||||
prompt: '',
|
||||
promptName: group ? group.name : '',
|
||||
category: group ? group.category : '',
|
||||
},
|
||||
});
|
||||
const { handleSubmit, setValue, reset, watch } = methods;
|
||||
const promptText = watch('prompt');
|
||||
|
||||
const selectedPrompt = useMemo(
|
||||
() => (prompts.length > 0 ? prompts[selectionIndex] : undefined),
|
||||
[prompts, /* eslint-disable-line react-hooks/exhaustive-deps */ selectionIndex],
|
||||
);
|
||||
|
||||
const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>();
|
||||
const hasShareAccess = useHasAccess({
|
||||
permissionType: PermissionTypes.PROMPTS,
|
||||
permission: Permissions.SHARED_GLOBAL,
|
||||
});
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: {
|
||||
prompt: '',
|
||||
promptName: group?.name || '',
|
||||
category: group?.category || '',
|
||||
},
|
||||
});
|
||||
|
||||
const { handleSubmit, setValue, reset, watch } = methods;
|
||||
const promptText = watch('prompt');
|
||||
|
||||
const createPromptMutation = useCreatePrompt({
|
||||
onMutate: (variables) => {
|
||||
reset(
|
||||
{
|
||||
prompt: variables.prompt.prompt,
|
||||
category: variables.group?.category || '',
|
||||
},
|
||||
{ keepDirtyValues: true },
|
||||
);
|
||||
},
|
||||
onSuccess(data) {
|
||||
if (alwaysMakeProd && data.prompt._id && data.prompt.groupId) {
|
||||
makeProductionMutation.mutate(
|
||||
{
|
||||
id: data.prompt._id,
|
||||
groupId: data.prompt.groupId,
|
||||
productionPrompt: { prompt: data.prompt.prompt },
|
||||
},
|
||||
{
|
||||
onSuccess: () => setSelectionIndex(0),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
reset({
|
||||
prompt: data.prompt.prompt,
|
||||
promptName: data.group?.name || '',
|
||||
category: data.group?.category || '',
|
||||
});
|
||||
|
||||
setSelectionIndex(0);
|
||||
},
|
||||
});
|
||||
const updateGroupMutation = useUpdatePromptGroup({
|
||||
onError: () => {
|
||||
showToast({
|
||||
|
|
@ -118,14 +87,34 @@ const PromptForm = () => {
|
|||
});
|
||||
},
|
||||
});
|
||||
|
||||
const makeProductionMutation = useMakePromptProduction();
|
||||
const deletePromptMutation = useDeletePrompt({
|
||||
onSuccess: (response) => {
|
||||
if (response.promptGroup) {
|
||||
navigate('/d/prompts');
|
||||
} else {
|
||||
setSelectionIndex(0);
|
||||
const deletePromptMutation = useDeletePrompt();
|
||||
|
||||
const createPromptMutation = useCreatePrompt({
|
||||
onMutate: (variables) => {
|
||||
reset(
|
||||
{
|
||||
prompt: variables.prompt.prompt,
|
||||
category: variables.group ? variables.group.category : '',
|
||||
},
|
||||
{ keepDirtyValues: true },
|
||||
);
|
||||
},
|
||||
onSuccess(data) {
|
||||
if (alwaysMakeProd && data.prompt._id && data.prompt.groupId) {
|
||||
makeProductionMutation.mutate({
|
||||
id: data.prompt._id,
|
||||
groupId: data.prompt.groupId,
|
||||
productionPrompt: { prompt: data.prompt.prompt },
|
||||
});
|
||||
}
|
||||
|
||||
reset({
|
||||
prompt: data.prompt.prompt,
|
||||
promptName: data.group ? data.group.name : '',
|
||||
category: data.group ? data.group.category : '',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -135,15 +124,18 @@ const PromptForm = () => {
|
|||
// TODO: show toast, cannot be empty.
|
||||
return;
|
||||
}
|
||||
if (!selectedPrompt) {
|
||||
return;
|
||||
}
|
||||
const tempPrompt: TCreatePrompt = {
|
||||
prompt: {
|
||||
type: selectedPrompt?.type ?? 'text',
|
||||
groupId: selectedPrompt?.groupId ?? '',
|
||||
type: selectedPrompt.type ?? 'text',
|
||||
groupId: selectedPrompt.groupId ?? '',
|
||||
prompt: value,
|
||||
},
|
||||
};
|
||||
|
||||
if (value === selectedPrompt?.prompt) {
|
||||
if (value === selectedPrompt.prompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -167,43 +159,45 @@ const PromptForm = () => {
|
|||
}, [isEditing, onSave, handleSubmit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorMode === PromptsEditorMode.SIMPLE) {
|
||||
const productionIndex = prompts.findIndex((prompt) => prompt._id === group?.productionId);
|
||||
setSelectionIndex(productionIndex !== -1 ? productionIndex : 0);
|
||||
}
|
||||
|
||||
handleLoadingComplete();
|
||||
}, [params.promptId, editorMode, group?.productionId, prompts, handleLoadingComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue('prompt', selectedPrompt?.prompt || '', { shouldDirty: false });
|
||||
setValue('category', group?.category || '', { shouldDirty: false });
|
||||
}, [selectedPrompt, group?.category, setValue]);
|
||||
setValue('prompt', selectedPrompt ? selectedPrompt.prompt : '', { shouldDirty: false });
|
||||
setValue('category', group ? group.category : '', { shouldDirty: false });
|
||||
}, [selectedPrompt, group, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.matchMedia('(min-width: 1022px)').matches) {
|
||||
setShowSidePanel(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const debouncedUpdateOneliner = useCallback(
|
||||
debounce((oneliner: string) => {
|
||||
if (!group) {
|
||||
if (!group || !group._id) {
|
||||
return console.warn('Group not found');
|
||||
}
|
||||
|
||||
updateGroupMutation.mutate({ id: group._id || '', payload: { oneliner } });
|
||||
updateGroupMutation.mutate({ id: group._id, payload: { oneliner } });
|
||||
}, 950),
|
||||
[updateGroupMutation, group],
|
||||
);
|
||||
|
||||
const debouncedUpdateCommand = useCallback(
|
||||
debounce((command: string) => {
|
||||
if (!group) {
|
||||
if (!group || !group._id) {
|
||||
return console.warn('Group not found');
|
||||
}
|
||||
|
||||
updateGroupMutation.mutate({ id: group._id || '', payload: { command } });
|
||||
updateGroupMutation.mutate({ id: group._id, payload: { command } });
|
||||
}, 950),
|
||||
[updateGroupMutation, group],
|
||||
);
|
||||
|
||||
const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>();
|
||||
|
||||
if (initialLoad) {
|
||||
return <SkeletonForm />;
|
||||
}
|
||||
|
|
@ -220,126 +214,192 @@ const PromptForm = () => {
|
|||
return <PromptDetails group={fetchedPrompt} />;
|
||||
}
|
||||
|
||||
if (!group) {
|
||||
if (!group || group._id == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const groupId = group._id;
|
||||
|
||||
const groupName = group.name;
|
||||
const groupCategory = group.category;
|
||||
|
||||
const RightPanel = () => (
|
||||
<div
|
||||
className="h-full w-full overflow-y-auto bg-surface-primary px-4"
|
||||
style={{ maxHeight: 'calc(100vh - 100px)' }}
|
||||
>
|
||||
<div className="mb-2 flex flex-col lg:flex-row lg:items-center lg:justify-center lg:gap-x-2 xl:flex-row xl:space-y-0">
|
||||
<CategorySelector
|
||||
currentCategory={groupCategory}
|
||||
onValueChange={(value) =>
|
||||
updateGroupMutation.mutate({
|
||||
id: groupId,
|
||||
payload: { name: groupName, category: value },
|
||||
})
|
||||
}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="mt-2 flex flex-row items-center justify-center gap-x-2 lg:mt-0">
|
||||
{hasShareAccess && <SharePrompt group={group} disabled={isLoadingGroup} />}
|
||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||
<Button
|
||||
variant="submit"
|
||||
size="sm"
|
||||
aria-label="Make prompt production"
|
||||
className="h-10 w-10 border border-transparent p-0.5 transition-all"
|
||||
onClick={() => {
|
||||
if (!selectedPrompt) {
|
||||
console.warn('No prompt is selected');
|
||||
return;
|
||||
}
|
||||
const { _id: promptVersionId = '', prompt } = selectedPrompt;
|
||||
makeProductionMutation.mutate({
|
||||
id: promptVersionId,
|
||||
groupId,
|
||||
productionPrompt: { prompt },
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
isLoadingGroup ||
|
||||
!selectedPrompt ||
|
||||
selectedPrompt._id === group.productionId ||
|
||||
makeProductionMutation.isLoading
|
||||
}
|
||||
>
|
||||
<Rocket className="size-5 cursor-pointer text-white" />
|
||||
</Button>
|
||||
)}
|
||||
<DeleteConfirm
|
||||
name={groupName}
|
||||
disabled={isLoadingGroup}
|
||||
selectHandler={() => {
|
||||
if (!selectedPrompt || !selectedPrompt._id) {
|
||||
console.warn('No prompt is selected or prompt _id is missing');
|
||||
return;
|
||||
}
|
||||
deletePromptMutation.mutate({
|
||||
_id: selectedPrompt._id,
|
||||
groupId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{editorMode === PromptsEditorMode.ADVANCED &&
|
||||
(isLoadingPrompts
|
||||
? Array.from({ length: 6 }).map((_, index: number) => (
|
||||
<div key={index} className="my-2">
|
||||
<Skeleton className="h-[72px] w-full" />
|
||||
</div>
|
||||
))
|
||||
: prompts.length > 0 && (
|
||||
<PromptVersions
|
||||
group={group}
|
||||
prompts={prompts}
|
||||
selectionIndex={selectionIndex}
|
||||
setSelectionIndex={setSelectionIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit((data) => onSave(data.prompt))}>
|
||||
<div>
|
||||
<div className="flex flex-col items-center justify-between px-4 dark:text-gray-200 sm:flex-row">
|
||||
{isLoadingGroup ? (
|
||||
<Skeleton className="mb-1 flex h-10 w-32 flex-row items-center font-bold sm:text-xl md:mb-0 md:h-12 md:text-2xl" />
|
||||
) : (
|
||||
<PromptName
|
||||
name={group.name}
|
||||
onSave={(value) => {
|
||||
if (!group) {
|
||||
return console.warn('Group not found');
|
||||
}
|
||||
updateGroupMutation.mutate({ id: group._id || '', payload: { name: value } });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex h-10 flex-row gap-x-2">
|
||||
<CategorySelector
|
||||
className="w-48 md:w-56"
|
||||
currentCategory={group.category}
|
||||
onValueChange={(value) =>
|
||||
updateGroupMutation.mutate({
|
||||
id: group._id || '',
|
||||
payload: { name: group.name || '', category: value },
|
||||
})
|
||||
}
|
||||
/>
|
||||
{hasShareAccess && <SharePrompt group={group} disabled={isLoadingGroup} />}
|
||||
<form className="mt-4 flex w-full" onSubmit={handleSubmit((data) => onSave(data.prompt))}>
|
||||
<div className="relative w-full">
|
||||
<div
|
||||
className="h-full w-full"
|
||||
style={{
|
||||
transform: `translateX(${showSidePanel ? `-${sidePanelWidth}` : '0'})`,
|
||||
transition: 'transform 0.3s ease-in-out',
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
<div className="flex-1 overflow-hidden px-4">
|
||||
<div className="mb-4 flex items-center gap-2 text-text-primary">
|
||||
{isLoadingGroup ? (
|
||||
<Skeleton className="mb-1 flex h-10 w-32 font-bold sm:text-xl md:mb-0 md:h-12 md:text-2xl" />
|
||||
) : (
|
||||
<>
|
||||
<PromptName
|
||||
name={groupName}
|
||||
onSave={(value) => {
|
||||
if (!group._id) {
|
||||
return;
|
||||
}
|
||||
updateGroupMutation.mutate({ id: group._id, payload: { name: value } });
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1" />
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 border border-border-light p-0 lg:hidden"
|
||||
onClick={() => setShowSidePanel(true)}
|
||||
aria-label={localize('com_ui_open_menu')}
|
||||
>
|
||||
<Menu className="size-5" />
|
||||
</Button>
|
||||
<div className="hidden lg:block">
|
||||
{editorMode === PromptsEditorMode.SIMPLE && <RightPanel />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isLoadingPrompts ? (
|
||||
<Skeleton className="h-96" aria-live="polite" />
|
||||
) : (
|
||||
<div className="flex h-full flex-col gap-4">
|
||||
<PromptEditor name="prompt" isEditing={isEditing} setIsEditing={setIsEditing} />
|
||||
<PromptVariables promptText={promptText} />
|
||||
<Description
|
||||
initialValue={group.oneliner ?? ''}
|
||||
onValueChange={debouncedUpdateOneliner}
|
||||
/>
|
||||
<Command
|
||||
initialValue={group.command ?? ''}
|
||||
onValueChange={debouncedUpdateCommand}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="h-10 w-10 border border-transparent bg-green-500 transition-all hover:bg-green-600 dark:bg-green-500 dark:hover:bg-green-600 p-0.5"
|
||||
onClick={() => {
|
||||
const { _id: promptVersionId = '', prompt } = selectedPrompt ?? ({} as TPrompt);
|
||||
makeProductionMutation.mutate(
|
||||
{
|
||||
id: promptVersionId || '',
|
||||
groupId: group._id || '',
|
||||
productionPrompt: { prompt },
|
||||
},
|
||||
{
|
||||
onSuccess: (_data, variables) => {
|
||||
const productionIndex = prompts.findIndex(
|
||||
(prompt) => variables.id === prompt._id,
|
||||
);
|
||||
setSelectionIndex(productionIndex);
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
disabled={
|
||||
isLoadingGroup ||
|
||||
selectedPrompt?._id === group.productionId ||
|
||||
makeProductionMutation.isLoading
|
||||
}
|
||||
>
|
||||
<Rocket className="cursor-pointer text-white size-5" />
|
||||
</Button>
|
||||
)}
|
||||
<DeleteConfirm
|
||||
name={group.name}
|
||||
disabled={isLoadingGroup}
|
||||
selectHandler={() => {
|
||||
deletePromptMutation.mutate({
|
||||
_id: selectedPrompt?._id || '',
|
||||
groupId: group._id || '',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||
<div className="mt-4 flex items-center justify-center text-text-primary sm:hidden">
|
||||
<AlwaysMakeProd />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex h-full w-full flex-col md:flex-row">
|
||||
{/* Left Section */}
|
||||
<div className="flex-1 overflow-y-auto border-gray-300 p-4 dark:border-gray-600 md:max-h-[calc(100vh-150px)] md:border-r">
|
||||
{isLoadingPrompts ? (
|
||||
<Skeleton className="h-96" />
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
<PromptEditor name="prompt" isEditing={isEditing} setIsEditing={setIsEditing} />
|
||||
<PromptVariables promptText={promptText} />
|
||||
<Description
|
||||
initialValue={group.oneliner ?? ''}
|
||||
onValueChange={debouncedUpdateOneliner}
|
||||
/>
|
||||
<Command
|
||||
initialValue={group.command ?? ''}
|
||||
onValueChange={debouncedUpdateCommand}
|
||||
/>
|
||||
<div className="hidden w-1/4 border-l border-border-light lg:block">
|
||||
<RightPanel />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Right Section */}
|
||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||
<div className="flex-1 overflow-y-auto p-4 md:max-h-[calc(100vh-150px)] md:w-1/4 md:max-w-[35%] lg:max-w-[30%] xl:max-w-[25%]">
|
||||
{isLoadingPrompts ? (
|
||||
<Skeleton className="h-96 w-full" />
|
||||
) : (
|
||||
!!prompts.length && (
|
||||
<PromptVersions
|
||||
group={group}
|
||||
prompts={prompts}
|
||||
selectionIndex={selectionIndex}
|
||||
setSelectionIndex={setSelectionIndex}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'absolute inset-0 z-40 cursor-default',
|
||||
showSidePanel ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
style={{ transition: 'opacity 0.3s ease-in-out' }}
|
||||
onClick={() => setShowSidePanel(false)}
|
||||
aria-hidden={!showSidePanel}
|
||||
tabIndex={showSidePanel ? 0 : -1}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute inset-y-0 right-0 z-50 lg:hidden"
|
||||
style={{
|
||||
width: sidePanelWidth,
|
||||
transform: `translateX(${showSidePanel ? '0' : '100%'})`,
|
||||
transition: 'transform 0.3s ease-in-out',
|
||||
}}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Mobile navigation panel"
|
||||
>
|
||||
<div className="h-full">
|
||||
<div className="h-full overflow-auto">
|
||||
<RightPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { EditIcon, SaveIcon } from '~/components/svg';
|
||||
import { Button, Label, Input, EditIcon, SaveIcon } from '~/components';
|
||||
|
||||
type Props = {
|
||||
name?: string;
|
||||
|
|
@ -31,21 +31,12 @@ const PromptName: React.FC<Props> = ({ name, onSave }) => {
|
|||
clearTimeout(blurTimeoutRef.current);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
blurTimeoutRef.current = setTimeout(() => {
|
||||
if (document.activeElement !== inputRef.current) {
|
||||
setIsEditing(false);
|
||||
setNewName(name);
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setIsEditing(false);
|
||||
setNewName(name);
|
||||
}
|
||||
if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
if (e.key === 'Enter') {
|
||||
saveName();
|
||||
}
|
||||
};
|
||||
|
|
@ -61,39 +52,64 @@ const PromptName: React.FC<Props> = ({ name, onSave }) => {
|
|||
}, [name]);
|
||||
|
||||
return (
|
||||
<div className="mb-1 flex flex-row items-center font-bold sm:text-xl md:mb-0 md:text-2xl">
|
||||
{isEditing ? (
|
||||
<div className="mb-1 flex items-center md:mb-0">
|
||||
<input
|
||||
type="text"
|
||||
value={newName ?? ''}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
ref={inputRef}
|
||||
className="mr-2 w-56 rounded-md border bg-transparent p-2 focus:outline-none dark:border-gray-600 md:w-auto"
|
||||
autoFocus={true}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveClick}
|
||||
className="rounded p-2 hover:bg-gray-300/50 dark:hover:bg-gray-700"
|
||||
>
|
||||
<SaveIcon className="icon-md" size="1.2em" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-1 flex items-center md:mb-0">
|
||||
<span className="border border-transparent p-2">{newName}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleEditClick}
|
||||
className="rounded p-2 hover:bg-gray-300/50 dark:hover:bg-gray-700"
|
||||
>
|
||||
<EditIcon className="icon-md" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
className="gap-2"
|
||||
>
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Input
|
||||
type="text"
|
||||
value={newName ?? ''}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
ref={inputRef}
|
||||
className="flex w-full max-w-none rounded-lg text-2xl font-bold transition duration-200"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleSaveClick}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-10 flex-shrink-0"
|
||||
aria-label="Save prompt name"
|
||||
>
|
||||
<SaveIcon className="icon-md" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Label
|
||||
className="text-2xl font-bold"
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{newName}
|
||||
</Label>
|
||||
<Button
|
||||
onClick={handleEditClick}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label="Edit prompt name"
|
||||
className="h-10 flex-shrink-0"
|
||||
>
|
||||
<EditIcon className="icon-md" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,18 +28,18 @@ const PromptVariables = ({
|
|||
}, [promptText]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="flex items-center gap-2 rounded-t-lg border border-border-medium py-2 pl-4 text-base font-semibold text-text-secondary">
|
||||
<div className="rounded-xl border border-border-light bg-transparent p-4 shadow-md ">
|
||||
<h3 className="flex items-center gap-2 py-2 text-lg font-semibold text-text-primary">
|
||||
<Variable className="icon-sm" />
|
||||
{localize('com_ui_variables')}
|
||||
</h3>
|
||||
<div className="flex w-full flex-row flex-wrap rounded-b-lg border border-border-medium p-4 md:min-h-16">
|
||||
<div className="flex flex-col space-y-4">
|
||||
{variables.length ? (
|
||||
<div className="flex h-7 items-center">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{variables.map((variable, index) => (
|
||||
<label
|
||||
<span
|
||||
className={cn(
|
||||
'mr-1 rounded-full border border-border-medium px-2 text-text-secondary',
|
||||
'rounded-full border border-border-light px-3 py-1 text-text-primary',
|
||||
specialVariables[variable.toLowerCase()] != null ? specialVariableClasses : '',
|
||||
)}
|
||||
key={index}
|
||||
|
|
@ -47,41 +47,34 @@ const PromptVariables = ({
|
|||
{specialVariables[variable.toLowerCase()] != null
|
||||
? variable.toLowerCase()
|
||||
: variable}
|
||||
</label>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-7 items-center">
|
||||
<span className="text-xs text-text-secondary md:text-sm">
|
||||
{/** @ts-ignore */}
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_variables_info')}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
<div className="text-sm text-text-secondary">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_variables_info')}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
<Separator className="my-3 bg-border-medium" />
|
||||
<Separator className="my-3 text-text-primary" />
|
||||
{showInfo && (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<span className="text-xs font-medium text-text-secondary md:text-sm">
|
||||
<span className="text-sm font-medium text-text-primary">
|
||||
{localize('com_ui_special_variables')}
|
||||
</span>
|
||||
{'\u00A0'}
|
||||
<span className="text-xs text-text-secondary md:text-sm">
|
||||
{/** @ts-ignore */}
|
||||
<span className="text-sm text-text-secondary">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_special_variables_info')}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs font-medium text-text-secondary md:text-sm">
|
||||
<span className="text-text-text-primary text-sm font-medium">
|
||||
{localize('com_ui_dropdown_variables')}
|
||||
</span>
|
||||
{'\u00A0'}
|
||||
<span className="text-xs text-text-secondary md:text-sm">
|
||||
{/** @ts-ignore */}
|
||||
<span className="text-sm text-text-secondary">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_dropdown_variables_info')}
|
||||
</ReactMarkdown>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,140 @@
|
|||
import React from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { Layers3 } from 'lucide-react';
|
||||
import { Layers3, Crown, Zap } from 'lucide-react';
|
||||
import type { TPrompt, TPromptGroup } from 'librechat-data-provider';
|
||||
import { Tag, TooltipAnchor, Label } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { Tag } from '~/components/ui';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const CombinedStatusIcon = ({ description }: { description: string }) => (
|
||||
<TooltipAnchor
|
||||
description={description}
|
||||
aria-label={description}
|
||||
render={
|
||||
<div className="flex items-center justify-center">
|
||||
<Crown className="h-4 w-4 text-amber-500" />
|
||||
</div>
|
||||
}
|
||||
></TooltipAnchor>
|
||||
);
|
||||
|
||||
const VersionTags = ({ tags }: { tags: string[] }) => {
|
||||
const localize = useLocalize();
|
||||
const isLatestAndProduction = tags.includes('latest') && tags.includes('production');
|
||||
|
||||
if (isLatestAndProduction) {
|
||||
return (
|
||||
<span className="absolute bottom-3 right-3">
|
||||
<CombinedStatusIcon description={localize('com_ui_latest_production_version')} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="flex gap-1 text-sm">
|
||||
{tags.map((tag, i) => (
|
||||
<TooltipAnchor
|
||||
description={
|
||||
tag === 'production'
|
||||
? localize('com_ui_currently_production')
|
||||
: localize('com_ui_latest_version')
|
||||
}
|
||||
key={`${tag}-${i}`}
|
||||
aria-label={
|
||||
tag === 'production'
|
||||
? localize('com_ui_currently_production')
|
||||
: localize('com_ui_latest_version')
|
||||
}
|
||||
render={
|
||||
<Tag
|
||||
label={tag}
|
||||
className={cn(
|
||||
'w-24 justify-center border border-transparent',
|
||||
tag === 'production'
|
||||
? 'bg-green-100 text-green-500 dark:border-green-500 dark:bg-transparent dark:text-green-500'
|
||||
: 'bg-blue-100 text-blue-500 dark:border-blue-500 dark:bg-transparent dark:text-blue-500',
|
||||
)}
|
||||
labelClassName="flex items-center m-0 justify-center gap-1"
|
||||
LabelNode={(() => {
|
||||
if (tag === 'production') {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<span className="slow-pulse size-2 rounded-full bg-green-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (tag === 'latest') {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Zap className="size-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
/>
|
||||
}
|
||||
></TooltipAnchor>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const VersionCard = ({
|
||||
prompt,
|
||||
index,
|
||||
isSelected,
|
||||
totalVersions,
|
||||
onClick,
|
||||
authorName,
|
||||
tags,
|
||||
}: {
|
||||
prompt: TPrompt;
|
||||
index: number;
|
||||
isSelected: boolean;
|
||||
totalVersions: number;
|
||||
onClick: () => void;
|
||||
authorName?: string;
|
||||
tags: string[];
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'group relative w-full rounded-lg border border-border-light p-4 transition-all duration-300',
|
||||
isSelected
|
||||
? 'bg-surface-hover shadow-xl'
|
||||
: 'bg-surface-primary shadow-sm hover:bg-surface-secondary',
|
||||
)}
|
||||
onClick={onClick}
|
||||
aria-selected={isSelected}
|
||||
role="tab"
|
||||
aria-label={localize('com_ui_version_var', `${totalVersions - index}`)}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-start justify-between lg:flex-col xl:flex-row">
|
||||
<h3 className="font-bold text-text-primary">
|
||||
{localize('com_ui_version_var', `${totalVersions - index}`)}
|
||||
</h3>
|
||||
<time className="text-xs text-text-secondary" dateTime={prompt.createdAt}>
|
||||
{format(new Date(prompt.createdAt), 'yyyy-MM-dd HH:mm')}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 lg:flex-col xl:flex-row">
|
||||
{authorName && (
|
||||
<Label className="text-left text-xs text-text-secondary">by {authorName}</Label>
|
||||
)}
|
||||
|
||||
{tags.length > 0 && <VersionTags tags={tags} />}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const PromptVersions = ({
|
||||
prompts,
|
||||
group,
|
||||
|
|
@ -14,19 +143,24 @@ const PromptVersions = ({
|
|||
}: {
|
||||
prompts: TPrompt[];
|
||||
group?: TPromptGroup;
|
||||
selectionIndex: React.SetStateAction<number>;
|
||||
selectionIndex: number;
|
||||
setSelectionIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="mb-4 flex gap-2 text-base font-semibold dark:text-gray-200">
|
||||
<Layers3 className="icon-lg text-green-500" />
|
||||
{localize('com_ui_versions')}
|
||||
</h2>
|
||||
<ul className="flex flex-col gap-3">
|
||||
<section className="my-6" aria-label="Prompt Versions">
|
||||
<header className="mb-6">
|
||||
<h2 className="flex items-center gap-2 text-base font-semibold text-text-primary">
|
||||
<Layers3 className="h-5 w-5 text-green-500" />
|
||||
{localize('com_ui_versions')}
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div className="flex flex-col gap-3" role="tablist" aria-label="Version history">
|
||||
{prompts.map((prompt: TPrompt, index: number) => {
|
||||
const tags: string[] = [];
|
||||
|
||||
if (index === 0) {
|
||||
tags.push('latest');
|
||||
}
|
||||
|
|
@ -36,53 +170,20 @@ const PromptVersions = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className={cn(
|
||||
'relative cursor-pointer rounded-lg border p-4 dark:border-gray-600 dark:bg-transparent',
|
||||
index === selectionIndex ? 'bg-gray-100 dark:bg-gray-700' : 'bg-white',
|
||||
)}
|
||||
<VersionCard
|
||||
key={prompt._id}
|
||||
prompt={prompt}
|
||||
index={index}
|
||||
isSelected={index === selectionIndex}
|
||||
totalVersions={prompts.length}
|
||||
onClick={() => setSelectionIndex(index)}
|
||||
>
|
||||
<p className="font-bold dark:text-gray-200">
|
||||
{localize('com_ui_version_var', `${prompts.length - index}`)}
|
||||
</p>
|
||||
<p className="absolute right-4 top-5 whitespace-nowrap text-xs text-gray-600 dark:text-gray-400">
|
||||
{format(new Date(prompt.createdAt), 'yyyy-MM-dd HH:mm')}
|
||||
</p>
|
||||
{tags.length > 0 && (
|
||||
<span className="flex flex-wrap gap-1 text-sm">
|
||||
{tags.map((tag, i) => {
|
||||
return (
|
||||
<Tag
|
||||
key={`${tag}-${i}`}
|
||||
label={tag}
|
||||
className={cn(
|
||||
'w-fit border border-transparent bg-blue-100 text-blue-500 dark:border-blue-500 dark:bg-transparent dark:text-blue-500',
|
||||
tag === 'production' &&
|
||||
'bg-green-100 text-green-500 dark:border-green-500 dark:bg-transparent dark:text-green-500',
|
||||
)}
|
||||
labelClassName="flex m-0 justify-center gap-1"
|
||||
LabelNode={
|
||||
tag === 'production' ? (
|
||||
<div className="flex items-center ">
|
||||
<span className="slow-pulse h-[0.4rem] w-[0.4rem] rounded-full bg-green-400" />
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{group?.authorName && (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">by {group.authorName}</p>
|
||||
)}
|
||||
</li>
|
||||
authorName={group?.authorName}
|
||||
tags={tags}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ export default function PromptsAccordion() {
|
|||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<PromptSidePanel className="lg:w-full xl:w-full" {...groupsNav}>
|
||||
<div className="flex w-full flex-row items-center justify-between px-2 pt-2">
|
||||
<div className="flex w-full flex-row items-center justify-between pt-2">
|
||||
<ManagePrompts className="select-none" />
|
||||
<AutoSendPrompt className="text-xs dark:text-white" />
|
||||
</div>
|
||||
<FilterPrompts setName={groupsNav.setName} className="items-center justify-center px-2" />
|
||||
<FilterPrompts setName={groupsNav.setName} className="items-center justify-center" />
|
||||
</PromptSidePanel>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useMemo, useEffect } from 'react';
|
||||
import { Outlet, useParams, useNavigate } from 'react-router-dom';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import AutoSendPrompt from '~/components/Prompts/Groups/AutoSendPrompt';
|
||||
import FilterPrompts from '~/components/Prompts/Groups/FilterPrompts';
|
||||
import DashBreadcrumb from '~/routes/Layouts/DashBreadcrumb';
|
||||
import { usePromptGroupsNav, useHasAccess } from '~/hooks';
|
||||
|
|
@ -35,13 +34,12 @@ export default function PromptsView() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col bg-[#f9f9f9] p-0 dark:bg-transparent lg:p-2">
|
||||
<div className="flex h-screen w-full flex-col bg-surface-primary p-0 lg:p-2">
|
||||
<DashBreadcrumb />
|
||||
<div className="flex w-full flex-grow flex-row divide-x overflow-hidden dark:divide-gray-600">
|
||||
<GroupSidePanel isDetailView={isDetailView} {...groupsNav}>
|
||||
<div className="mx-2 mt-1 flex flex-row items-center justify-between">
|
||||
<FilterPrompts setName={groupsNav.setName} />
|
||||
<AutoSendPrompt className="text-xs text-text-primary" />
|
||||
</div>
|
||||
</GroupSidePanel>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -8,14 +8,15 @@ import type {
|
|||
TUpdatePromptGroupPayload,
|
||||
} from 'librechat-data-provider';
|
||||
import {
|
||||
Button,
|
||||
Switch,
|
||||
OGDialog,
|
||||
OGDialogTitle,
|
||||
OGDialogClose,
|
||||
OGDialogContent,
|
||||
OGDialogTrigger,
|
||||
OGDialogClose,
|
||||
} from '~/components/ui';
|
||||
import { useUpdatePromptGroup, useGetStartupConfig } from '~/data-provider';
|
||||
import { Button, Switch } from '~/components/ui';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
|
|
@ -30,14 +31,13 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
|
|||
const { data: startupConfig = {} as TStartupConfig, isFetching } = useGetStartupConfig();
|
||||
const { instanceProjectId } = startupConfig;
|
||||
const groupIsGlobal = useMemo(
|
||||
() => !!(group?.projectIds ?? []).includes(instanceProjectId),
|
||||
() => ((group?.projectIds ?? []) as string[]).includes(instanceProjectId as string),
|
||||
[group, instanceProjectId],
|
||||
);
|
||||
|
||||
const {
|
||||
control,
|
||||
setValue,
|
||||
getValues,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<FormValues>({
|
||||
|
|
@ -51,19 +51,26 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
|
|||
setValue(Permissions.SHARED_GLOBAL, groupIsGlobal);
|
||||
}, [groupIsGlobal, setValue]);
|
||||
|
||||
if (!group || !instanceProjectId) {
|
||||
if (group == null || !instanceProjectId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onSubmit = (data: FormValues) => {
|
||||
const groupId = group._id ?? '';
|
||||
if (!groupId || !instanceProjectId) {
|
||||
if (groupId === '' || !instanceProjectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data[Permissions.SHARED_GLOBAL] === true && groupIsGlobal) {
|
||||
showToast({
|
||||
message: localize('com_ui_prompt_already_shared_to_all'),
|
||||
status: 'info',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {} as TUpdatePromptGroupPayload;
|
||||
|
||||
if (data[Permissions.SHARED_GLOBAL]) {
|
||||
if (data[Permissions.SHARED_GLOBAL] === true) {
|
||||
payload.projectIds = [startupConfig.instanceProjectId];
|
||||
} else {
|
||||
payload.removeProjectIds = [startupConfig.instanceProjectId];
|
||||
|
|
@ -81,81 +88,50 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
|
|||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
aria-label="Share prompt"
|
||||
className="h-10 w-10 border border-transparent bg-blue-500/90 p-0.5 transition-all hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-800"
|
||||
disabled={disabled}
|
||||
>
|
||||
<Share2Icon className="size-5 cursor-pointer text-white" />
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogContent className="w-11/12 max-w-[600px]">
|
||||
<OGDialogTitle className="truncate pr-2" title={group.name}>
|
||||
<OGDialogContent className="w-11/12 max-w-lg" role="dialog" aria-labelledby="dialog-title">
|
||||
<OGDialogTitle id="dialog-title" className="truncate pr-2" title={group.name}>
|
||||
{localize('com_ui_share_var', `"${group.name}"`)}
|
||||
</OGDialogTitle>
|
||||
<form className="p-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<form className="p-2" onSubmit={handleSubmit(onSubmit)} aria-describedby="form-description">
|
||||
<div id="form-description" className="sr-only">
|
||||
{localize('com_ui_share_form_description')}
|
||||
</div>
|
||||
<div className="mb-4 flex items-center justify-between gap-2 py-4">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="mr-2 cursor-pointer"
|
||||
onClick={() =>
|
||||
setValue(Permissions.SHARED_GLOBAL, !getValues(Permissions.SHARED_GLOBAL), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setValue(Permissions.SHARED_GLOBAL, !getValues(Permissions.SHARED_GLOBAL), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}
|
||||
}}
|
||||
aria-checked={getValues(Permissions.SHARED_GLOBAL)}
|
||||
role="checkbox"
|
||||
>
|
||||
{localize('com_ui_share_to_all_users')}
|
||||
</button>
|
||||
<label htmlFor={Permissions.SHARED_GLOBAL} className="select-none">
|
||||
{groupIsGlobal && (
|
||||
<span className="ml-2 text-xs">{localize('com_ui_prompt_shared_to_all')}</span>
|
||||
)}
|
||||
</label>
|
||||
<div className="flex items-center" id="share-to-all-users">
|
||||
{localize('com_ui_share_to_all_users')}
|
||||
</div>
|
||||
<Controller
|
||||
name={Permissions.SHARED_GLOBAL}
|
||||
control={control}
|
||||
disabled={isFetching || updateGroup.isLoading || !instanceProjectId}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
const isValid = !(value && groupIsGlobal);
|
||||
if (!isValid) {
|
||||
showToast({
|
||||
message: localize('com_ui_prompt_already_shared_to_all'),
|
||||
status: 'warning',
|
||||
});
|
||||
}
|
||||
return isValid;
|
||||
},
|
||||
}}
|
||||
disabled={isFetching === true || updateGroup.isLoading || !instanceProjectId}
|
||||
render={({ field }) => (
|
||||
<Switch
|
||||
{...field}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
value={field.value.toString()}
|
||||
aria-labelledby="share-to-all-users"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<OGDialogClose asChild>
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || isFetching}
|
||||
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
|
||||
variant="submit"
|
||||
aria-label={localize('com_ui_save')}
|
||||
>
|
||||
{localize('com_ui_save')}
|
||||
</button>
|
||||
</Button>
|
||||
</OGDialogClose>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,59 +1,66 @@
|
|||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { Search } from 'lucide-react';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const AnimatedSearchInput = ({ value, onChange, isSearching: searching, placeholder }) => {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const AnimatedSearchInput = ({
|
||||
value,
|
||||
onChange,
|
||||
isSearching: searching,
|
||||
placeholder,
|
||||
}: {
|
||||
value?: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
isSearching?: boolean;
|
||||
placeholder: string;
|
||||
}) => {
|
||||
const isSearching = searching === true;
|
||||
const hasValue = value != null && value.length > 0;
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<div className="relative rounded-lg transition-all duration-500 ease-in-out">
|
||||
{/* Background gradient effect */}
|
||||
<div
|
||||
className={`
|
||||
absolute inset-0 rounded-lg
|
||||
bg-gradient-to-r from-blue-500/20 via-purple-500/20 to-blue-500/20
|
||||
transition-all duration-500 ease-in-out
|
||||
${isSearching ? 'opacity-100 blur-sm' : 'opacity-0 blur-none'}
|
||||
`}
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute left-3 top-1/2 z-10 -translate-y-1/2">
|
||||
{/* Icon on the left */}
|
||||
<div className="absolute left-3 top-1/2 z-50 -translate-y-1/2">
|
||||
<Search
|
||||
className={`
|
||||
h-4 w-4 transition-all duration-500 ease-in-out
|
||||
${isFocused ? 'text-blue-500' : 'text-gray-400'}
|
||||
${isSearching ? 'text-blue-400' : ''}
|
||||
`}
|
||||
className={cn(
|
||||
`
|
||||
h-4 w-4 transition-all duration-500 ease-in-out`,
|
||||
isSearching && hasValue ? 'text-blue-400' : 'text-gray-400',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Input field with background transitions */}
|
||||
{/* Input field */}
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
placeholder={placeholder}
|
||||
className={`
|
||||
w-full rounded-lg px-10 py-2
|
||||
transition-all duration-500 ease-in-out
|
||||
placeholder:text-gray-400
|
||||
peer relative z-20 w-full rounded-lg bg-surface-secondary px-10
|
||||
py-2 outline-none ring-0 backdrop-blur-sm transition-all
|
||||
duration-500 ease-in-out placeholder:text-gray-400
|
||||
focus:outline-none focus:ring-0
|
||||
${isFocused ? 'bg-white/10' : 'bg-white/5'}
|
||||
${isSearching ? 'bg-white/15' : ''}
|
||||
backdrop-blur-sm
|
||||
`}
|
||||
/>
|
||||
|
||||
{/* Gradient overlay */}
|
||||
<div
|
||||
className={`
|
||||
pointer-events-none absolute inset-0 z-20 rounded-lg
|
||||
bg-gradient-to-r from-blue-500/20 via-purple-500/20 to-blue-500/20
|
||||
transition-all duration-500 ease-in-out
|
||||
${isSearching && hasValue ? 'opacity-100 blur-sm' : 'opacity-0 blur-none'}
|
||||
`}
|
||||
/>
|
||||
|
||||
{/* Animated loading indicator */}
|
||||
<div
|
||||
className={`
|
||||
absolute right-3 top-1/2 -translate-y-1/2
|
||||
absolute right-3 top-1/2 z-20 -translate-y-1/2
|
||||
transition-all duration-500 ease-in-out
|
||||
${isSearching ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}
|
||||
${isSearching && hasValue ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}
|
||||
`}
|
||||
>
|
||||
<div className="relative h-2 w-2">
|
||||
|
|
@ -69,7 +76,7 @@ const AnimatedSearchInput = ({ value, onChange, isSearching: searching, placehol
|
|||
className={`
|
||||
absolute -inset-8 -z-10
|
||||
transition-all duration-700 ease-in-out
|
||||
${isSearching ? 'scale-105 opacity-100' : 'scale-100 opacity-0'}
|
||||
${isSearching && hasValue ? 'scale-105 opacity-100' : 'scale-100 opacity-0'}
|
||||
`}
|
||||
>
|
||||
<div className="absolute inset-0">
|
||||
|
|
@ -77,26 +84,24 @@ const AnimatedSearchInput = ({ value, onChange, isSearching: searching, placehol
|
|||
className={`
|
||||
bg-gradient-radial absolute inset-0 from-blue-500/10 to-transparent
|
||||
transition-opacity duration-700 ease-in-out
|
||||
${isSearching ? 'animate-pulse-slow opacity-100' : 'opacity-0'}
|
||||
${isSearching && hasValue ? 'animate-pulse-slow opacity-100' : 'opacity-0'}
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
absolute inset-0 bg-gradient-to-r from-purple-500/5 via-blue-500/5 to-purple-500/5
|
||||
blur-xl transition-all duration-700 ease-in-out
|
||||
${isSearching ? 'animate-gradient-x opacity-100' : 'opacity-0'}
|
||||
${isSearching && hasValue ? 'animate-gradient-x opacity-100' : 'opacity-0'}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Focus state background glow */}
|
||||
<div
|
||||
className={`
|
||||
absolute inset-0 -z-20 bg-gradient-to-r from-blue-500/10
|
||||
via-purple-500/10 to-blue-500/10 blur-xl
|
||||
absolute inset-0 -z-20 scale-100 bg-gradient-to-r from-blue-500/10
|
||||
via-purple-500/10 to-blue-500/10 opacity-0 blur-xl
|
||||
transition-all duration-500 ease-in-out
|
||||
${isFocused ? 'scale-105 opacity-100' : 'scale-100 opacity-0'}
|
||||
peer-focus:scale-105 peer-focus:opacity-100
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ const buttonVariants = cva(
|
|||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
destructive:
|
||||
'bg-surface-destructive text-destructive-foreground hover:bg-surface-destructive-hover',
|
||||
outline:
|
||||
'text-text-primary border border-border-light bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
|
|
|
|||
|
|
@ -7,12 +7,22 @@ interface DropdownProps {
|
|||
value?: string;
|
||||
label?: string;
|
||||
onChange: (value: string) => void;
|
||||
options: string[] | Option[];
|
||||
options: (string | Option | { divider: true })[];
|
||||
className?: string;
|
||||
sizeClasses?: string;
|
||||
testId?: string;
|
||||
icon?: React.ReactNode;
|
||||
iconOnly?: boolean;
|
||||
renderValue?: (option: Option) => React.ReactNode;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
const isDivider = (item: string | Option | { divider: true }): item is { divider: true } =>
|
||||
typeof item === 'object' && 'divider' in item;
|
||||
|
||||
const isOption = (item: string | Option | { divider: true }): item is Option =>
|
||||
typeof item === 'object' && 'value' in item && 'label' in item;
|
||||
|
||||
const Dropdown: React.FC<DropdownProps> = ({
|
||||
value: selectedValue,
|
||||
label = '',
|
||||
|
|
@ -21,6 +31,10 @@ const Dropdown: React.FC<DropdownProps> = ({
|
|||
className = '',
|
||||
sizeClasses,
|
||||
testId = 'dropdown-menu',
|
||||
icon,
|
||||
iconOnly = false,
|
||||
renderValue,
|
||||
ariaLabel,
|
||||
}) => {
|
||||
const handleChange = (value: string) => {
|
||||
onChange(value);
|
||||
|
|
@ -31,63 +45,101 @@ const Dropdown: React.FC<DropdownProps> = ({
|
|||
setValue: handleChange,
|
||||
});
|
||||
|
||||
const getOptionObject = (val: string | undefined): Option | undefined => {
|
||||
if (val == null || val === '') {
|
||||
return undefined;
|
||||
}
|
||||
return options
|
||||
.filter((o) => !isDivider(o))
|
||||
.map((o) => (typeof o === 'string' ? { value: o, label: o } : o))
|
||||
.find((o) => isOption(o) && o.value === val) as Option | undefined;
|
||||
};
|
||||
|
||||
const getOptionLabel = (currentValue: string | undefined) => {
|
||||
if (currentValue == null || currentValue === '') {
|
||||
return '';
|
||||
}
|
||||
const option = getOptionObject(currentValue);
|
||||
return option ? option.label : currentValue;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<Select.Select
|
||||
store={selectProps}
|
||||
className={cn(
|
||||
'focus:ring-offset-ring-offset relative inline-flex w-auto items-center justify-between rounded-lg border border-input bg-background py-2 pl-3 pr-8 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 items-center justify-between rounded-lg 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',
|
||||
iconOnly ? 'h-full w-10' : 'w-fit gap-2',
|
||||
className,
|
||||
)}
|
||||
data-testid={testId}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="block truncate">
|
||||
{label}
|
||||
{options
|
||||
.map((o) => (typeof o === 'string' ? { value: o, label: o } : o))
|
||||
.find((o) => o.value === selectedValue)?.label ?? selectedValue}
|
||||
</span>
|
||||
<Select.SelectArrow />
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{icon}
|
||||
{!iconOnly && (
|
||||
<span className="block truncate">
|
||||
{label}
|
||||
{(() => {
|
||||
const matchedOption = getOptionObject(selectedValue);
|
||||
if (matchedOption && renderValue) {
|
||||
return renderValue(matchedOption);
|
||||
}
|
||||
return getOptionLabel(selectedValue);
|
||||
})()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!iconOnly && <Select.SelectArrow />}
|
||||
</Select.Select>
|
||||
<Select.SelectPopover
|
||||
portal
|
||||
store={selectProps}
|
||||
className={cn('popover-ui', sizeClasses, className)}
|
||||
className={cn('popover-ui', sizeClasses, className, 'max-h-[80vh] overflow-y-auto')}
|
||||
>
|
||||
{options.map((item, index) => (
|
||||
<Select.SelectItem
|
||||
key={index}
|
||||
value={typeof item === 'string' ? item : item.value}
|
||||
className="select-item"
|
||||
data-theme={typeof item === 'string' ? item : (item as Option).value}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="block truncate">
|
||||
{typeof item === 'string' ? item : (item as Option).label}
|
||||
</span>
|
||||
{selectedValue === (typeof item === 'string' ? item : item.value) && (
|
||||
<span className="ml-auto pl-2">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon-md block group-hover:hidden"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Select.SelectItem>
|
||||
))}
|
||||
{options.map((item, index) => {
|
||||
if (isDivider(item)) {
|
||||
return <div key={`divider-${index}`} className="my-1 border-t border-border-heavy" />;
|
||||
}
|
||||
|
||||
const option = typeof item === 'string' ? { value: item, label: item } : item;
|
||||
if (!isOption(option)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select.SelectItem
|
||||
key={`option-${index}`}
|
||||
value={String(option.value)}
|
||||
className="select-item"
|
||||
data-theme={option.value}
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{option.icon != null && <span>{option.icon as React.ReactNode}</span>}
|
||||
<span className="block truncate">{option.label}</span>
|
||||
{selectedValue === option.value && (
|
||||
<span className="ml-auto pl-2">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon-md block group-hover:hidden"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Select.SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select.SelectPopover>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...pr
|
|||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const Label = React.forwardRef<
|
|||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-200',
|
||||
'block w-full break-all text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-200',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,17 @@ import {
|
|||
OGDialogHeader,
|
||||
OGDialogContent,
|
||||
OGDialogDescription,
|
||||
OGDialog,
|
||||
} from './OriginalDialog';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { Spinner } from '../svg';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
type SelectionProps = {
|
||||
selectHandler?: () => void;
|
||||
selectClasses?: string;
|
||||
selectText?: string | ReactNode;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
type DialogTemplateProps = {
|
||||
|
|
@ -30,6 +33,7 @@ type DialogTemplateProps = {
|
|||
footerClassName?: string;
|
||||
showCloseButton?: boolean;
|
||||
showCancelButton?: boolean;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDivElement>) => {
|
||||
|
|
@ -49,7 +53,7 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
|
|||
overlayClassName,
|
||||
showCancelButton = true,
|
||||
} = props;
|
||||
const { selectHandler, selectClasses, selectText } = selection || {};
|
||||
const { selectHandler, selectClasses, selectText, isLoading } = selection || {};
|
||||
const Cancel = localize('com_ui_cancel');
|
||||
|
||||
const defaultSelect =
|
||||
|
|
@ -83,11 +87,12 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
|
|||
{selection ? (
|
||||
<OGDialogClose
|
||||
onClick={selectHandler}
|
||||
disabled={isLoading}
|
||||
className={`${
|
||||
selectClasses ?? defaultSelect
|
||||
} flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm max-sm:order-first max-sm:w-full sm:order-none`}
|
||||
} flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm disabled:opacity-80 max-sm:order-first max-sm:w-full sm:order-none`}
|
||||
>
|
||||
{selectText}
|
||||
{isLoading === true ? <Spinner className="size-4 text-white" /> : selectText}
|
||||
</OGDialogClose>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,29 +32,31 @@ export const useUpdatePromptGroup = (
|
|||
mutationFn: (variables: t.TUpdatePromptGroupVariables) =>
|
||||
dataService.updatePromptGroup(variables),
|
||||
onMutate: (variables: t.TUpdatePromptGroupVariables) => {
|
||||
const group = JSON.parse(
|
||||
JSON.stringify(
|
||||
queryClient.getQueryData<t.TPromptGroup>([QueryKeys.promptGroup, variables.id]),
|
||||
),
|
||||
) as t.TPromptGroup;
|
||||
const groupData = queryClient.getQueryData<t.PromptGroupListData>([
|
||||
const groupData = queryClient.getQueryData<t.TPromptGroup>([
|
||||
QueryKeys.promptGroup,
|
||||
variables.id,
|
||||
]);
|
||||
const group = groupData ? structuredClone(groupData) : undefined;
|
||||
|
||||
const groupListData = queryClient.getQueryData<t.PromptGroupListData>([
|
||||
QueryKeys.promptGroups,
|
||||
name,
|
||||
category,
|
||||
pageSize,
|
||||
]);
|
||||
const previousListData = JSON.parse(JSON.stringify(groupData)) as t.PromptGroupListData;
|
||||
const previousListData = groupListData ? structuredClone(groupListData) : undefined;
|
||||
|
||||
let update = variables.payload;
|
||||
if (update.removeProjectIds && group.projectIds) {
|
||||
update = JSON.parse(JSON.stringify(update));
|
||||
if (update.removeProjectIds && group?.projectIds) {
|
||||
update = structuredClone(update);
|
||||
update.projectIds = group.projectIds.filter((id) => !update.removeProjectIds?.includes(id));
|
||||
delete update.removeProjectIds;
|
||||
}
|
||||
|
||||
if (groupData) {
|
||||
if (groupListData) {
|
||||
const newData = updateGroupFields(
|
||||
/* Paginated Data */
|
||||
groupData,
|
||||
groupListData,
|
||||
/* Update */
|
||||
{ _id: variables.id, ...update },
|
||||
/* Callback */
|
||||
|
|
@ -74,12 +76,12 @@ export const useUpdatePromptGroup = (
|
|||
},
|
||||
onError: (err, variables, context) => {
|
||||
if (context?.group) {
|
||||
queryClient.setQueryData([QueryKeys.promptGroups, variables.id], context?.group);
|
||||
queryClient.setQueryData([QueryKeys.promptGroups, variables.id], context.group);
|
||||
}
|
||||
if (context?.previousListData) {
|
||||
queryClient.setQueryData<t.PromptGroupListData>(
|
||||
[QueryKeys.promptGroups, name, category, pageSize],
|
||||
context?.previousListData,
|
||||
context.previousListData,
|
||||
);
|
||||
}
|
||||
if (onError) {
|
||||
|
|
@ -110,7 +112,7 @@ export const useCreatePrompt = (
|
|||
onSuccess: (response, variables, context) => {
|
||||
const { prompt, group } = response;
|
||||
queryClient.setQueryData(
|
||||
[QueryKeys.prompts, variables?.prompt?.groupId],
|
||||
[QueryKeys.prompts, variables.prompt.groupId],
|
||||
(oldData: t.TPrompt[] | undefined) => {
|
||||
return [prompt, ...(oldData ?? [])];
|
||||
},
|
||||
|
|
@ -301,12 +303,12 @@ export const useMakePromptProduction = (options?: t.MakePromptProductionOptions)
|
|||
},
|
||||
onError: (err, variables, context) => {
|
||||
if (context?.group) {
|
||||
queryClient.setQueryData([QueryKeys.promptGroups, variables.groupId], context?.group);
|
||||
queryClient.setQueryData([QueryKeys.promptGroups, variables.groupId], context.group);
|
||||
}
|
||||
if (context?.previousListData) {
|
||||
queryClient.setQueryData<t.PromptGroupListData>(
|
||||
[QueryKeys.promptGroups, name, category, pageSize],
|
||||
context?.previousListData,
|
||||
context.previousListData,
|
||||
);
|
||||
}
|
||||
if (onError) {
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@ const useCategories = (className = '') => {
|
|||
const { data: categories = loadingCategories } = useGetCategories({
|
||||
select: (data) =>
|
||||
data.map((category) => ({
|
||||
label: category.label
|
||||
? localize(`com_ui_${category.label}`) || category.label
|
||||
: localize('com_ui_select_a_category'),
|
||||
label: localize(`com_ui_${category.label}`) || category.label,
|
||||
value: category.value,
|
||||
icon: category.value ? (
|
||||
<CategoryIcon category={category.value} className={className} />
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ export default {
|
|||
com_ui_prompt: 'Prompt',
|
||||
com_ui_prompts: 'Prompts',
|
||||
com_ui_prompt_name: 'Prompt Name',
|
||||
com_ui_rename_prompt: 'Rename Prompt',
|
||||
com_ui_delete_prompt: 'Delete Prompt?',
|
||||
com_ui_admin: 'Admin',
|
||||
com_ui_simple: 'Simple',
|
||||
|
|
@ -229,9 +230,12 @@ export default {
|
|||
com_ui_prompt_name_required: 'Prompt Name is required',
|
||||
com_ui_prompt_text_required: 'Text is required',
|
||||
com_ui_prompt_text: 'Text',
|
||||
com_ui_currently_production: 'Currently in production',
|
||||
com_ui_latest_version: 'Latest version',
|
||||
com_ui_back_to_chat: 'Back to Chat',
|
||||
com_ui_back_to_prompts: 'Back to Prompts',
|
||||
com_ui_categories: 'Categories',
|
||||
com_ui_filter_prompts: 'Filter Prompts',
|
||||
com_ui_filter_prompts_name: 'Filter prompts by name',
|
||||
com_ui_search_categories: 'Search Categories',
|
||||
com_ui_manage: 'Manage',
|
||||
|
|
@ -373,9 +377,13 @@ export default {
|
|||
com_ui_agent_duplicate_error: 'There was an error duplicating the agent',
|
||||
com_ui_prompt_already_shared_to_all: 'This prompt is already shared to all users',
|
||||
com_ui_description_placeholder: 'Optional: Enter a description to display for the prompt',
|
||||
com_ui_command_placeholder: 'Optional: Enter a command for the prompt or name will be used.',
|
||||
com_ui_command_placeholder: 'Optional: Enter a command for the prompt or name will be used',
|
||||
com_ui_command_usage_placeholder: 'Select a Prompt by command or name',
|
||||
com_ui_no_prompt_description: 'No description found.',
|
||||
com_ui_latest_production_version: 'Latest production version',
|
||||
com_ui_confirm_change: 'Confirm Change',
|
||||
com_ui_confirm_admin_use_change:
|
||||
'Changing this setting will block access for admins, including yourself. Are you sure you want to proceed?',
|
||||
com_ui_share_link_to_chat: 'Share link to chat',
|
||||
com_ui_share_error: 'There was an error sharing the chat link',
|
||||
com_ui_share_retrieve_error: 'There was an error retrieving the shared links',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useSetRecoilState } from 'recoil';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { SystemRoles } from 'librechat-data-provider';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { ArrowLeft, MessageSquareQuote } from 'lucide-react';
|
||||
import {
|
||||
Breadcrumb,
|
||||
|
|
@ -17,8 +17,10 @@ import {
|
|||
} from '~/components/ui';
|
||||
import { useLocalize, useCustomLink, useAuthContext } from '~/hooks';
|
||||
import AdvancedSwitch from '~/components/Prompts/AdvancedSwitch';
|
||||
import { RightPanel } from '../../components/Prompts/RightPanel';
|
||||
import AdminSettings from '~/components/Prompts/AdminSettings';
|
||||
import { useDashboardContext } from '~/Providers';
|
||||
import { PromptsEditorMode } from '~/common';
|
||||
import store from '~/store';
|
||||
|
||||
const promptsPathPattern = /prompts\/(?!new(?:\/|$)).*$/;
|
||||
|
|
@ -40,6 +42,7 @@ export default function DashBreadcrumb() {
|
|||
|
||||
const setPromptsName = useSetRecoilState(store.promptsName);
|
||||
const setPromptsCategory = useSetRecoilState(store.promptsCategory);
|
||||
const editorMode = useRecoilValue(store.promptsEditorMode);
|
||||
|
||||
const clickCallback = useCallback(() => {
|
||||
setPromptsName('');
|
||||
|
|
@ -55,7 +58,7 @@ export default function DashBreadcrumb() {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="mr-4 flex h-10 items-center justify-between">
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -29,6 +29,17 @@
|
|||
--green-800: #065f46;
|
||||
--green-900: #064e3b;
|
||||
--green-950: #022c22;
|
||||
--red-50: #fef2f2;
|
||||
--red-100: #fee2e2;
|
||||
--red-200: #fecaca;
|
||||
--red-300: #fca5a5;
|
||||
--red-400: #f87171;
|
||||
--red-500: #ef4444;
|
||||
--red-600: #dc2626;
|
||||
--red-700: #b91c1c;
|
||||
--red-800: #991b1b;
|
||||
--red-900: #7f1d1d;
|
||||
--red-950: #450a0a;
|
||||
--gizmo-gray-500: #999;
|
||||
--gizmo-gray-600: #666;
|
||||
--gizmo-gray-950: #0f0f0f;
|
||||
|
|
@ -62,6 +73,8 @@ html {
|
|||
--surface-dialog: var(--white);
|
||||
--surface-submit: var(--green-700);
|
||||
--surface-submit-hover: var(--green-800);
|
||||
--surface-destructive: var(--red-700);
|
||||
--surface-destructive-hover: var(--red-800);
|
||||
--border-light: var(--gray-200);
|
||||
--border-medium-alt: var(--gray-300);
|
||||
--border-medium: var(--gray-300);
|
||||
|
|
@ -117,6 +130,8 @@ html {
|
|||
--surface-dialog: var(--gray-850);
|
||||
--surface-submit: var(--green-700);
|
||||
--surface-submit-hover: var(--green-800);
|
||||
--surface-destructive: var(--red-800);
|
||||
--surface-destructive-hover: var(--red-900);
|
||||
--border-light: var(--gray-700);
|
||||
--border-medium-alt: var(--gray-600);
|
||||
--border-medium: var(--gray-600);
|
||||
|
|
@ -2347,7 +2362,7 @@ button.scroll-convo {
|
|||
.popover-ui {
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
max-height: min(var(--popover-available-height, 300px), 300px);
|
||||
max-height: min(var(--popover-available-height, 1700px), 1700px);
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
|
@ -2418,9 +2433,15 @@ button.scroll-convo {
|
|||
/** AnimatedSearchInput style */
|
||||
|
||||
@keyframes gradient-x {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-gradient-x {
|
||||
|
|
@ -2452,4 +2473,4 @@ button.scroll-convo {
|
|||
|
||||
.scale-98 {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ module.exports = {
|
|||
'surface-dialog': 'var(--surface-dialog)',
|
||||
'surface-submit': 'var(--surface-submit)',
|
||||
'surface-submit-hover': 'var(--surface-submit-hover)',
|
||||
'surface-destructive': 'var(--surface-destructive)',
|
||||
'surface-destructive-hover': 'var(--surface-destructive-hover)',
|
||||
'border-light': 'var(--border-light)',
|
||||
'border-medium': 'var(--border-medium)',
|
||||
'border-medium-alt': 'var(--border-medium-alt)',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue