mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-26 05:08:50 +01:00
* chore: add i18n localization comment for AlwaysMakeProd component * feat: enhance accessibility by adding aria-label and aria-labelledby to Switch component * feat: add aria-labels for accessibility in Agent and Assistant avatar buttons * fix: add switch aria-labels for accessibility in various components * feat: add aria-labels and localization keys for accessibility in DataTable, DataTableColumnHeader, and OGDialogTemplate components * chore: refactor out nested ternary * feat: add aria-label to DataTable filter button for My Files modal * feat: add aria-labels for Buttons and localization strings * feat: add aria-labels to Checkboxes in Agent Builder * feat: enhance accessibility by adding aria-label and aria-labelledby to Checkbox component * feat: add aria-label to FileSearchCheckbox in Agent Builder * feat: add aria-label to Prompts text input area * feat: enhance accessibility by adding aria-label and aria-labelledby to TextAreaAutosize component * feat: remove improper role: "list" prop from List in Conversations.tsx to enhance accessibility and stop aria rules conflicting within react-virtualized component * feat: enhance accessibility by allowing tab navigation and adding ring highlights for conversation title editing accept/reject buttons * feat: add aria-label to Copy Link button in the conversation share modal * feat: add title to QR code svg in conversation share modal to describe the image content * feat: enhance accessibility by making Agent Avatar upload keyboard navigable and round out highlight border on focus * feat: enhance accessibility by adding aria attributes around alerting users with screen readers to invalid email address inputs in the Agent Builder * feat: add aria-labels to buttons in Advanced panel of Agent Builder * feat: enhance accessibility by making FileUpload and Clear All buttons in PresetItems keyboard navigable * feat: enchance accessiblity by indexing view and delete button aria-labels in shared links management modal to their specific chat titles * feat: add border highlighting on focus for AnimatedSearchInput * feat: add category description to aria-labels for prompts in ListCard * feat: add proper scoping to rows and columns in table headers * feat: add localized aria-labelling to EditTextPart's TextAreaAutosize component and base dynamic paramters panel components and their supporting translation keys * feat: add localized aria-labels and aria-labelledBy to Checkbox components without them * feat: add localized aria-labeledBy for endpoint settings Sliders * feat: add localized aria-labels for TextareaAutosize components * chore: remove unused i18n string * feat: add localized aria-label for BookmarkForm Checkbox * fix: add stopPropagation onKeyDown for Preview and Edit menu items in prompts that was causing the prompts to inadvertently be sent when triggered with keyboard navigation when Auto-send Prompts was toggled on * fix: switch TableCell to TableHead for title cells according to harvard issue #789 * fix: add more descriptive localization key for file filter button in DataTable * chore: remove self-explanatory code comment from RenameForm * fix: remove stray bg-yellow highlight that was left in during debugging * fix: add aria-label to model configurator panel back button * fix: undo incorrect hoist of tool name split for aria-label and span in MCPInput --------- Co-authored-by: Danny Avila <danny@librechat.ai>
206 lines
6.7 KiB
TypeScript
206 lines
6.7 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
|
import {
|
|
OGDialog,
|
|
OGDialogTemplate,
|
|
Button,
|
|
Label,
|
|
Input,
|
|
Spinner,
|
|
useToastContext,
|
|
} from '@librechat/client';
|
|
import type { TUserMemory } from 'librechat-data-provider';
|
|
import { useUpdateMemoryMutation, useMemoriesQuery } from '~/data-provider';
|
|
import { useLocalize, useHasAccess } from '~/hooks';
|
|
|
|
interface MemoryEditDialogProps {
|
|
memory: TUserMemory | null;
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
children: React.ReactNode;
|
|
triggerRef?: React.MutableRefObject<HTMLButtonElement | null>;
|
|
}
|
|
|
|
export default function MemoryEditDialog({
|
|
memory,
|
|
open,
|
|
onOpenChange,
|
|
children,
|
|
triggerRef,
|
|
}: MemoryEditDialogProps) {
|
|
const localize = useLocalize();
|
|
const { showToast } = useToastContext();
|
|
const { data: memData } = useMemoriesQuery();
|
|
|
|
const hasUpdateAccess = useHasAccess({
|
|
permissionType: PermissionTypes.MEMORIES,
|
|
permission: Permissions.UPDATE,
|
|
});
|
|
|
|
const { mutate: updateMemory, isLoading } = useUpdateMemoryMutation({
|
|
onMutate: () => {
|
|
onOpenChange(false);
|
|
setTimeout(() => {
|
|
triggerRef?.current?.focus();
|
|
}, 0);
|
|
},
|
|
onSuccess: () => {
|
|
showToast({
|
|
message: localize('com_ui_saved'),
|
|
status: 'success',
|
|
});
|
|
},
|
|
onError: (error: Error) => {
|
|
let errorMessage = localize('com_ui_error');
|
|
|
|
if (error && typeof error === 'object' && 'response' in error) {
|
|
const axiosError = error as any;
|
|
if (axiosError.response?.data?.error) {
|
|
errorMessage = axiosError.response.data.error;
|
|
|
|
// Check for duplicate key error
|
|
if (axiosError.response?.status === 409 || errorMessage.includes('already exists')) {
|
|
errorMessage = localize('com_ui_memory_key_exists');
|
|
}
|
|
// Check for key validation error (lowercase and underscores only)
|
|
else if (errorMessage.includes('lowercase letters and underscores')) {
|
|
errorMessage = localize('com_ui_memory_key_validation');
|
|
}
|
|
}
|
|
} else if (error.message) {
|
|
errorMessage = error.message;
|
|
}
|
|
|
|
showToast({
|
|
message: errorMessage,
|
|
status: 'error',
|
|
});
|
|
},
|
|
});
|
|
|
|
const [key, setKey] = useState('');
|
|
const [value, setValue] = useState('');
|
|
const [originalKey, setOriginalKey] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (memory) {
|
|
setKey(memory.key);
|
|
setValue(memory.value);
|
|
setOriginalKey(memory.key);
|
|
}
|
|
}, [memory]);
|
|
|
|
const handleSave = () => {
|
|
if (!hasUpdateAccess || !memory) {
|
|
return;
|
|
}
|
|
|
|
if (!key.trim() || !value.trim()) {
|
|
showToast({
|
|
message: localize('com_ui_field_required'),
|
|
status: 'error',
|
|
});
|
|
return;
|
|
}
|
|
|
|
updateMemory({
|
|
key: key.trim(),
|
|
value: value.trim(),
|
|
...(originalKey !== key.trim() && { originalKey }),
|
|
});
|
|
};
|
|
|
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && e.ctrlKey && hasUpdateAccess) {
|
|
handleSave();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<OGDialog open={open} onOpenChange={onOpenChange} triggerRef={triggerRef}>
|
|
{children}
|
|
<OGDialogTemplate
|
|
title={hasUpdateAccess ? localize('com_ui_edit_memory') : localize('com_ui_view_memory')}
|
|
showCloseButton={false}
|
|
className="w-11/12 md:max-w-lg"
|
|
main={
|
|
<div className="space-y-4">
|
|
{memory && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-xs text-text-secondary">
|
|
<div>
|
|
{localize('com_ui_date')}:{' '}
|
|
{new Date(memory.updated_at).toLocaleDateString(undefined, {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})}
|
|
</div>
|
|
{/* Token Information */}
|
|
{memory.tokenCount !== undefined && (
|
|
<div>
|
|
{memory.tokenCount.toLocaleString()}
|
|
{memData?.tokenLimit && ` / ${memData.tokenLimit.toLocaleString()}`}{' '}
|
|
{localize(memory.tokenCount === 1 ? 'com_ui_token' : 'com_ui_tokens')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{/* Overall Memory Usage */}
|
|
{memData?.tokenLimit && memData?.usagePercentage !== null && (
|
|
<div className="text-xs text-text-secondary">
|
|
{localize('com_ui_usage')}: {memData.usagePercentage}%{' '}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="memory-key" className="text-sm font-medium">
|
|
{localize('com_ui_key')}
|
|
</Label>
|
|
<Input
|
|
id="memory-key"
|
|
value={key}
|
|
onChange={(e) => hasUpdateAccess && setKey(e.target.value)}
|
|
onKeyDown={handleKeyPress}
|
|
placeholder={localize('com_ui_enter_key')}
|
|
className="w-full"
|
|
disabled={!hasUpdateAccess}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="memory-value" className="text-sm font-medium">
|
|
{localize('com_ui_value')}
|
|
</Label>
|
|
<textarea
|
|
id="memory-value"
|
|
value={value}
|
|
onChange={(e) => hasUpdateAccess && setValue(e.target.value)}
|
|
onKeyDown={handleKeyPress}
|
|
placeholder={localize('com_ui_enter_value')}
|
|
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
rows={3}
|
|
disabled={!hasUpdateAccess}
|
|
/>
|
|
</div>
|
|
</div>
|
|
}
|
|
buttons={
|
|
hasUpdateAccess ? (
|
|
<Button
|
|
type="button"
|
|
variant="submit"
|
|
onClick={handleSave}
|
|
aria-label={localize('com_ui_save')}
|
|
disabled={isLoading || !key.trim() || !value.trim()}
|
|
className="text-white"
|
|
>
|
|
{isLoading ? <Spinner className="size-4" /> : localize('com_ui_save')}
|
|
</Button>
|
|
) : null
|
|
}
|
|
/>
|
|
</OGDialog>
|
|
);
|
|
}
|