mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
✨ style: Enhance Styling & Accessibility (#5956)
* ✨ feat: Enhance UI Components with Shadows and Accessibility Improvements * 🔧 fix: Correct Category Labels and Values in API Model & Adjust Button Class in Prompt List
This commit is contained in:
parent
fdb3cf3f58
commit
fe7013562b
9 changed files with 59 additions and 47 deletions
|
|
@ -3,40 +3,40 @@ const { logger } = require('~/config');
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
label: 'idea',
|
label: 'com_ui_idea',
|
||||||
value: 'com_ui_idea',
|
value: 'idea',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'travel',
|
label: 'com_ui_travel',
|
||||||
value: 'com_ui_travel',
|
value: 'travel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'teach_or_explain',
|
label: 'com_ui_teach_or_explain',
|
||||||
value: 'com_ui_teach_or_explain',
|
value: 'teach_or_explain',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'write',
|
label: 'com_ui_write',
|
||||||
value: 'com_ui_write',
|
value: 'write',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'shop',
|
label: 'com_ui_shop',
|
||||||
value: 'com_ui_shop',
|
value: 'shop',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'code',
|
label: 'com_ui_code',
|
||||||
value: 'com_ui_code',
|
value: 'code',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'misc',
|
label: 'com_ui_misc',
|
||||||
value: 'com_ui_misc',
|
value: 'misc',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'roleplay',
|
label: 'com_ui_roleplay',
|
||||||
value: 'com_ui_roleplay',
|
value: 'roleplay',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'finance',
|
label: 'com_ui_finance',
|
||||||
value: 'com_ui_finance',
|
value: 'finance',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ const Command = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border-light">
|
<div className="rounded-xl border border-border-light shadow-md">
|
||||||
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
||||||
<SquareSlash className="icon-sm" aria-hidden="true" />
|
<SquareSlash className="icon-sm" aria-hidden="true" />
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ const Description = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border-light">
|
<div className="rounded-xl border border-border-light shadow-md">
|
||||||
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
||||||
<Info className="icon-sm" aria-hidden="true" />
|
<Info className="icon-sm" aria-hidden="true" />
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export default function List({
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full bg-transparent px-3"
|
className={`w-full bg-transparent ${isChatRoute ? '' : 'mx-2'}`}
|
||||||
onClick={() => navigate('/d/prompts/new')}
|
onClick={() => navigate('/d/prompts/new')}
|
||||||
>
|
>
|
||||||
<Plus className="size-4" aria-hidden />
|
<Plus className="size-4" aria-hidden />
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full flex-1 overflow-auto rounded-b-xl border border-border-light p-2 transition-all duration-150 sm:p-4',
|
'w-full flex-1 overflow-auto rounded-b-xl border border-border-light p-2 shadow-md transition-all duration-150 sm:p-4',
|
||||||
{
|
{
|
||||||
'cursor-pointer bg-surface-primary hover:bg-surface-secondary active:bg-surface-tertiary':
|
'cursor-pointer bg-surface-primary hover:bg-surface-secondary active:bg-surface-tertiary':
|
||||||
!isEditing,
|
!isEditing,
|
||||||
|
|
@ -105,6 +105,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||||
isEditing ? (
|
isEditing ? (
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
{...field}
|
{...field}
|
||||||
|
autoFocus
|
||||||
className="w-full resize-none overflow-y-auto rounded bg-transparent text-sm text-text-primary focus:outline-none sm:text-base"
|
className="w-full resize-none overflow-y-auto rounded bg-transparent text-sm text-text-primary focus:outline-none sm:text-base"
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={14}
|
maxRows={14}
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,6 @@ const PromptForm = () => {
|
||||||
payload: { name: groupName, category: value },
|
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">
|
<div className="mt-2 flex flex-row items-center justify-center gap-x-2 lg:mt-0">
|
||||||
{hasShareAccess && <SharePrompt group={group} disabled={isLoadingGroup} />}
|
{hasShareAccess && <SharePrompt group={group} disabled={isLoadingGroup} />}
|
||||||
|
|
@ -349,7 +348,7 @@ const PromptForm = () => {
|
||||||
{isLoadingPrompts ? (
|
{isLoadingPrompts ? (
|
||||||
<Skeleton className="h-96" aria-live="polite" />
|
<Skeleton className="h-96" aria-live="polite" />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full flex-col gap-4">
|
<div className="mb-2 flex h-full flex-col gap-4">
|
||||||
<PromptEditor name="prompt" isEditing={isEditing} setIsEditing={setIsEditing} />
|
<PromptEditor name="prompt" isEditing={isEditing} setIsEditing={setIsEditing} />
|
||||||
<PromptVariables promptText={promptText} />
|
<PromptVariables promptText={promptText} />
|
||||||
<Description
|
<Description
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ const PromptVariables = ({
|
||||||
}, [promptText]);
|
}, [promptText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border-light bg-transparent p-4 shadow-md ">
|
<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">
|
<h3 className="flex items-center gap-2 py-2 text-lg font-semibold text-text-primary">
|
||||||
<Variable className="icon-sm" aria-hidden="true" />
|
<Variable className="icon-sm" aria-hidden="true" />
|
||||||
{localize('com_ui_variables')}
|
{localize('com_ui_variables')}
|
||||||
|
|
@ -71,7 +71,7 @@ const PromptVariables = ({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-text-text-primary text-sm font-medium">
|
<span className="text-sm font-medium text-text-primary">
|
||||||
{localize('com_ui_dropdown_variables')}
|
{localize('com_ui_dropdown_variables')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-text-secondary">
|
<span className="text-sm text-text-secondary">
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export default function Parameters({
|
||||||
return value ?? '';
|
return value ?? '';
|
||||||
}, [providerOption]);
|
}, [providerOption]);
|
||||||
const models = useMemo(
|
const models = useMemo(
|
||||||
() => (provider ? modelsData[provider] ?? [] : []),
|
() => (provider ? (modelsData[provider] ?? []) : []),
|
||||||
[modelsData, provider],
|
[modelsData, provider],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -99,6 +99,7 @@ export default function Parameters({
|
||||||
{/* Endpoint aka Provider for Agents */}
|
{/* Endpoint aka Provider for Agents */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label
|
<label
|
||||||
|
id="provider-label"
|
||||||
className="text-token-text-primary model-panel-label mb-2 block font-medium"
|
className="text-token-text-primary model-panel-label mb-2 block font-medium"
|
||||||
htmlFor="provider"
|
htmlFor="provider"
|
||||||
>
|
>
|
||||||
|
|
@ -111,6 +112,10 @@ export default function Parameters({
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<>
|
<>
|
||||||
<SelectDropDown
|
<SelectDropDown
|
||||||
|
id="provider"
|
||||||
|
aria-labelledby="provider-label"
|
||||||
|
aria-label={localize('com_ui_provider')}
|
||||||
|
aria-required="true"
|
||||||
emptyTitle={true}
|
emptyTitle={true}
|
||||||
value={field.value ?? ''}
|
value={field.value ?? ''}
|
||||||
title={localize('com_ui_provider')}
|
title={localize('com_ui_provider')}
|
||||||
|
|
@ -140,6 +145,7 @@ export default function Parameters({
|
||||||
{/* Model */}
|
{/* Model */}
|
||||||
<div className="model-panel-section mb-4">
|
<div className="model-panel-section mb-4">
|
||||||
<label
|
<label
|
||||||
|
id="model-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-token-text-primary model-panel-label mb-2 block font-medium',
|
'text-token-text-primary model-panel-label mb-2 block font-medium',
|
||||||
!provider && 'text-gray-500 dark:text-gray-400',
|
!provider && 'text-gray-500 dark:text-gray-400',
|
||||||
|
|
@ -155,6 +161,10 @@ export default function Parameters({
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<>
|
<>
|
||||||
<SelectDropDown
|
<SelectDropDown
|
||||||
|
id="model"
|
||||||
|
aria-labelledby="model-label"
|
||||||
|
aria-label={localize('com_ui_model')}
|
||||||
|
aria-required="true"
|
||||||
emptyTitle={true}
|
emptyTitle={true}
|
||||||
placeholder={
|
placeholder={
|
||||||
provider
|
provider
|
||||||
|
|
@ -188,7 +198,6 @@ export default function Parameters({
|
||||||
{parameters && (
|
{parameters && (
|
||||||
<div className="h-auto max-w-full overflow-x-hidden p-2">
|
<div className="h-auto max-w-full overflow-x-hidden p-2">
|
||||||
<div className="grid grid-cols-4 gap-6">
|
<div className="grid grid-cols-4 gap-6">
|
||||||
{' '}
|
|
||||||
{/* This is the parent element containing all settings */}
|
{/* This is the parent element containing all settings */}
|
||||||
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
|
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
|
||||||
{parameters.map((setting) => {
|
{parameters.map((setting) => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Label,
|
Label,
|
||||||
Listbox,
|
Listbox,
|
||||||
|
|
@ -82,18 +82,14 @@ function SelectDropDown({
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = _title;
|
let title = _title;
|
||||||
|
|
||||||
if (emptyTitle) {
|
if (emptyTitle) {
|
||||||
title = '';
|
title = '';
|
||||||
} else if (!(title ?? '')) {
|
} else if (!(title ?? '')) {
|
||||||
title = localize('com_ui_model');
|
title = localize('com_ui_model');
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = availableValues ?? [];
|
const values = availableValues ?? [];
|
||||||
|
|
||||||
// Detemine if we should to convert this component into a searchable select. If we have enough elements, a search
|
// Enable searchable select if enough items are provided.
|
||||||
// input will appear near the top of the menu, allowing correct filtering of different model menu items. This will
|
|
||||||
// reset once the component is unmounted (as per a normal search)
|
|
||||||
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>({
|
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>({
|
||||||
availableOptions: values,
|
availableOptions: values,
|
||||||
placeholder: searchPlaceholder,
|
placeholder: searchPlaceholder,
|
||||||
|
|
@ -103,26 +99,35 @@ function SelectDropDown({
|
||||||
});
|
});
|
||||||
const hasSearchRender = searchRender != null;
|
const hasSearchRender = searchRender != null;
|
||||||
const options = hasSearchRender ? filteredValues : values;
|
const options = hasSearchRender ? filteredValues : values;
|
||||||
|
|
||||||
const renderIcon = showOptionIcon && value != null && (value as OptionWithIcon).icon != null;
|
const renderIcon = showOptionIcon && value != null && (value as OptionWithIcon).icon != null;
|
||||||
|
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center justify-center gap-2 ', containerClassName ?? '')}>
|
<div className={cn('flex items-center justify-center gap-2', containerClassName ?? '')}>
|
||||||
<div className={cn('relative w-full', subContainerClassName ?? '')}>
|
<div className={cn('relative w-full', subContainerClassName ?? '')}>
|
||||||
<Listbox value={value} onChange={setValue} disabled={disabled}>
|
<Listbox value={value} onChange={setValue} disabled={disabled}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
|
ref={buttonRef}
|
||||||
data-testid="select-dropdown-button"
|
data-testid="select-dropdown-button"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!open && buttonRef.current) {
|
||||||
|
buttonRef.current.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left disabled:bg-white dark:border-gray-600 dark:bg-gray-700 sm:text-sm',
|
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:bg-white dark:border-gray-600 dark:bg-gray-700 sm:text-sm',
|
||||||
className ?? '',
|
className ?? '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{' '}
|
|
||||||
{showLabel && (
|
{showLabel && (
|
||||||
<Label
|
<Label
|
||||||
className="block text-xs text-gray-700 dark:text-gray-500 "
|
className="block text-xs text-gray-700 dark:text-gray-500"
|
||||||
id="headlessui-listbox-label-:r1:"
|
id="headlessui-listbox-label-:r1:"
|
||||||
data-headlessui-state=""
|
data-headlessui-state=""
|
||||||
>
|
>
|
||||||
|
|
@ -154,11 +159,9 @@ function SelectDropDown({
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return <span className="text-text-secondary">{placeholder}</span>;
|
return <span className="text-text-secondary">{placeholder}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return value.label ?? '';
|
return value.label ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
})()}
|
})()}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -171,7 +174,7 @@ function SelectDropDown({
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className="h-4 w-4 text-gray-400"
|
className="h-4 w-4 text-gray-400"
|
||||||
height="1em"
|
height="1em"
|
||||||
width="1em"
|
width="1em"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -212,17 +215,17 @@ function SelectDropDown({
|
||||||
if (!option) {
|
if (!option) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLabel =
|
const currentLabel =
|
||||||
typeof option === 'string' ? option : option.label ?? option.value ?? '';
|
typeof option === 'string' ? option : (option.label ?? option.value ?? '');
|
||||||
const currentValue = typeof option === 'string' ? option : option.value ?? '';
|
const currentValue = typeof option === 'string' ? option : (option.value ?? '');
|
||||||
const currentIcon =
|
const currentIcon =
|
||||||
typeof option === 'string' ? null : (option.icon as React.ReactNode) ?? null;
|
typeof option === 'string'
|
||||||
|
? null
|
||||||
|
: ((option.icon as React.ReactNode) ?? null);
|
||||||
let activeValue: string | number | null | Option = value;
|
let activeValue: string | number | null | Option = value;
|
||||||
if (typeof activeValue !== 'string') {
|
if (typeof activeValue !== 'string') {
|
||||||
activeValue = activeValue?.value ?? '';
|
activeValue = activeValue?.value ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListboxOption
|
<ListboxOption
|
||||||
key={i}
|
key={i}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue