mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00: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>
|
||||
<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,18 +145,19 @@ 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"
|
||||
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-1/4 border-border-light bg-surface-primary text-text-primary">
|
||||
<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>
|
||||
|
|
@ -161,7 +170,7 @@ const AdminSettings = () => {
|
|||
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">
|
||||
<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>
|
||||
}
|
||||
|
|
@ -180,6 +189,14 @@ const AdminSettings = () => {
|
|||
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 && (
|
||||
<>
|
||||
|
|
@ -190,9 +207,10 @@ const AdminSettings = () => {
|
|||
href="https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/interface"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 underline"
|
||||
className="inline-flex items-center text-blue-500 underline"
|
||||
>
|
||||
{localize('com_ui_more_info')}
|
||||
<ExternalLink size={16} className="ml-1" />
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -201,18 +219,43 @@ const AdminSettings = () => {
|
|||
))}
|
||||
</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"
|
||||
>
|
||||
<Button type="submit" disabled={isSubmitting || isLoading} variant="submit">
|
||||
{localize('com_ui_save')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</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,224 +1,169 @@
|
|||
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: () => {
|
||||
if (blurTimeoutRef.current) {
|
||||
clearTimeout(blurTimeoutRef.current);
|
||||
setNameEditFlag(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
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 = (e: React.KeyboardEvent) => {
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
navigate(`/d/prompts/${group._id}`, { replace: true });
|
||||
}
|
||||
};
|
||||
},
|
||||
[group._id, navigate],
|
||||
);
|
||||
|
||||
const handleRename = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
setNameEditFlag(true);
|
||||
};
|
||||
const triggerDelete = useCallback(() => {
|
||||
deleteGroup.mutate({ id: group._id ?? '' });
|
||||
}, [group._id, deleteGroup]);
|
||||
|
||||
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 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">
|
||||
<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}
|
||||
</h3>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{groupIsGlobal === true && (
|
||||
|
||||
<div className="flex h-full items-center gap-2">
|
||||
{isGlobalGroup && (
|
||||
<EarthIcon
|
||||
className="icon-md text-green-400"
|
||||
className="icon-md text-green-500"
|
||||
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',
|
||||
)}
|
||||
variant="ghost"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={localize('com_ui_delete_prompt')}
|
||||
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
||||
aria-label={localize('com_ui_rename_prompt') + ' ' + group.name}
|
||||
>
|
||||
<TrashIcon
|
||||
className="icon-md text-gray-600 dark:text-gray-300"
|
||||
aria-hidden="true"
|
||||
<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}
|
||||
/>
|
||||
</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="max-w-[450px]"
|
||||
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 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,
|
||||
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'),
|
||||
|
|
@ -229,13 +174,8 @@ export default function DashGroupItem({
|
|||
)}
|
||||
</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>
|
||||
</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">
|
||||
<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')}
|
||||
<div className="flex flex-row gap-6">
|
||||
</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,9 +117,17 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={cn('overflow-y-auto text-sm sm:text-base')}
|
||||
style={{ minHeight: '4.5em', maxHeight: '21em', overflow: 'auto' }}
|
||||
>
|
||||
<ReactMarkdown
|
||||
/** @ts-ignore */
|
||||
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
||||
remarkPlugins={[
|
||||
supersub,
|
||||
remarkGfm,
|
||||
[remarkMath, { singleDollarTextMath: true }],
|
||||
]}
|
||||
/** @ts-ignore */
|
||||
rehypePlugins={rehypePlugins}
|
||||
/** @ts-ignore */
|
||||
|
|
@ -117,6 +136,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
>
|
||||
{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,96 +214,142 @@ const PromptForm = () => {
|
|||
return <PromptDetails group={fetchedPrompt} />;
|
||||
}
|
||||
|
||||
if (!group) {
|
||||
if (!group || group._id == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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">
|
||||
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
|
||||
className="w-48 md:w-56"
|
||||
currentCategory={group.category}
|
||||
currentCategory={groupCategory}
|
||||
onValueChange={(value) =>
|
||||
updateGroupMutation.mutate({
|
||||
id: group._id || '',
|
||||
payload: { name: group.name || '', category: value },
|
||||
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="default"
|
||||
variant="submit"
|
||||
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"
|
||||
aria-label="Make prompt production"
|
||||
className="h-10 w-10 border border-transparent p-0.5 transition-all"
|
||||
onClick={() => {
|
||||
const { _id: promptVersionId = '', prompt } = selectedPrompt ?? ({} as TPrompt);
|
||||
makeProductionMutation.mutate(
|
||||
{
|
||||
id: promptVersionId || '',
|
||||
groupId: group._id || '',
|
||||
if (!selectedPrompt) {
|
||||
console.warn('No prompt is selected');
|
||||
return;
|
||||
}
|
||||
const { _id: promptVersionId = '', prompt } = selectedPrompt;
|
||||
makeProductionMutation.mutate({
|
||||
id: promptVersionId,
|
||||
groupId,
|
||||
productionPrompt: { prompt },
|
||||
},
|
||||
{
|
||||
onSuccess: (_data, variables) => {
|
||||
const productionIndex = prompts.findIndex(
|
||||
(prompt) => variables.id === prompt._id,
|
||||
);
|
||||
setSelectionIndex(productionIndex);
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
isLoadingGroup ||
|
||||
selectedPrompt?._id === group.productionId ||
|
||||
!selectedPrompt ||
|
||||
selectedPrompt._id === group.productionId ||
|
||||
makeProductionMutation.isLoading
|
||||
}
|
||||
>
|
||||
<Rocket className="cursor-pointer text-white size-5" />
|
||||
<Rocket className="size-5 cursor-pointer text-white" />
|
||||
</Button>
|
||||
)}
|
||||
<DeleteConfirm
|
||||
name={group.name}
|
||||
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: group._id || '',
|
||||
_id: selectedPrompt._id,
|
||||
groupId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||
<div className="mt-4 flex items-center justify-center text-text-primary sm:hidden">
|
||||
<AlwaysMakeProd />
|
||||
{editorMode === PromptsEditorMode.ADVANCED &&
|
||||
(isLoadingPrompts
|
||||
? Array.from({ length: 6 }).map((_, index: number) => (
|
||||
<div key={index} className="my-2">
|
||||
<Skeleton className="h-[72px] w-full" />
|
||||
</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" />
|
||||
))
|
||||
: prompts.length > 0 && (
|
||||
<PromptVersions
|
||||
group={group}
|
||||
prompts={prompts}
|
||||
selectionIndex={selectionIndex}
|
||||
setSelectionIndex={setSelectionIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<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" />
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
<>
|
||||
<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
|
||||
|
|
@ -323,25 +363,45 @@ const PromptForm = () => {
|
|||
</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 className="hidden w-1/4 border-l border-border-light lg:block">
|
||||
<RightPanel />
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
</FormProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,40 +52,65 @@ 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">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
className="gap-2"
|
||||
>
|
||||
{isEditing ? (
|
||||
<div className="mb-1 flex items-center md:mb-0">
|
||||
<input
|
||||
<>
|
||||
<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}
|
||||
className="flex w-full max-w-none rounded-lg text-2xl font-bold transition duration-200"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
<Button
|
||||
onClick={handleSaveClick}
|
||||
className="rounded p-2 hover:bg-gray-300/50 dark:hover:bg-gray-700"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-10 flex-shrink-0"
|
||||
aria-label="Save prompt name"
|
||||
>
|
||||
<SaveIcon className="icon-md" size="1.2em" />
|
||||
</button>
|
||||
</div>
|
||||
<SaveIcon className="icon-md" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className="mb-1 flex items-center md:mb-0">
|
||||
<span className="border border-transparent p-2">{newName}</span>
|
||||
<button
|
||||
type="button"
|
||||
<>
|
||||
<Label
|
||||
className="text-2xl font-bold"
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{newName}
|
||||
</Label>
|
||||
<Button
|
||||
onClick={handleEditClick}
|
||||
className="rounded p-2 hover:bg-gray-300/50 dark:hover:bg-gray-700"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label="Edit prompt name"
|
||||
className="h-10 flex-shrink-0"
|
||||
>
|
||||
<EditIcon className="icon-md" />
|
||||
</button>
|
||||
</div>
|
||||
</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 */}
|
||||
<div className="text-sm text-text-secondary">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_variables_info')}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
</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" />
|
||||
<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>
|
||||
<ul className="flex flex-col gap-3">
|
||||
</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
|
||||
}
|
||||
authorName={group?.authorName}
|
||||
tags={tags}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{group?.authorName && (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">by {group.authorName}</p>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</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"
|
||||
>
|
||||
<div className="flex items-center" id="share-to-all-users">
|
||||
{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>
|
||||
<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,42 +45,79 @@ 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">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{icon}
|
||||
{!iconOnly && (
|
||||
<span className="block truncate">
|
||||
{label}
|
||||
{options
|
||||
.map((o) => (typeof o === 'string' ? { value: o, label: o } : o))
|
||||
.find((o) => o.value === selectedValue)?.label ?? selectedValue}
|
||||
{(() => {
|
||||
const matchedOption = getOptionObject(selectedValue);
|
||||
if (matchedOption && renderValue) {
|
||||
return renderValue(matchedOption);
|
||||
}
|
||||
return getOptionLabel(selectedValue);
|
||||
})()}
|
||||
</span>
|
||||
<Select.SelectArrow />
|
||||
)}
|
||||
</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) => (
|
||||
{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={index}
|
||||
value={typeof item === 'string' ? item : item.value}
|
||||
key={`option-${index}`}
|
||||
value={String(option.value)}
|
||||
className="select-item"
|
||||
data-theme={typeof item === 'string' ? item : (item as Option).value}
|
||||
data-theme={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) && (
|
||||
<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"
|
||||
|
|
@ -87,7 +138,8 @@ const Dropdown: React.FC<DropdownProps> = ({
|
|||
)}
|
||||
</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 {
|
||||
|
|
|
|||
|
|
@ -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