mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 01:10:14 +01:00
✍️ feat: Automatic Save and Restore for Chat (#2942)
* feat: added "Save draft locally" to Message settings * feat: add hook to save chat input as draft every second * fix: use filepath if the file does not have a preview prop * fix: not to delete temporary files when navigating to a new chat * chore: translations * chore: import order * chore: import order --------- Co-authored-by: Danny Avila <danacordially@gmail.com> Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
e9bbf39618
commit
29e71e98ad
22 changed files with 345 additions and 5 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { memo, useCallback, useRef, useMemo } from 'react';
|
import { memo, useCallback, useRef, useMemo, useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
supportsFiles,
|
supportsFiles,
|
||||||
mergeFileConfig,
|
mergeFileConfig,
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
fileConfig as defaultFileConfig,
|
fileConfig as defaultFileConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||||
|
import { useAutoSave } from '~/hooks/Input/useAutoSave';
|
||||||
import { useRequiresKey, useTextarea } from '~/hooks';
|
import { useRequiresKey, useTextarea } from '~/hooks';
|
||||||
import { TextareaAutosize } from '~/components/ui';
|
import { TextareaAutosize } from '~/components/ui';
|
||||||
import { useGetFileConfig } from '~/data-provider';
|
import { useGetFileConfig } from '~/data-provider';
|
||||||
|
|
@ -56,6 +57,14 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
handleStopGenerating,
|
handleStopGenerating,
|
||||||
} = useChatContext();
|
} = useChatContext();
|
||||||
|
|
||||||
|
const { clearDraft } = useAutoSave({
|
||||||
|
conversationId: useMemo(() => conversation?.conversationId, [conversation]),
|
||||||
|
textAreaRef,
|
||||||
|
setValue: methods.setValue,
|
||||||
|
files,
|
||||||
|
setFiles,
|
||||||
|
});
|
||||||
|
|
||||||
const assistantMap = useAssistantsMapContext();
|
const assistantMap = useAssistantsMapContext();
|
||||||
|
|
||||||
const submitMessage = useCallback(
|
const submitMessage = useCallback(
|
||||||
|
|
@ -65,8 +74,9 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
}
|
}
|
||||||
ask({ text: data.text });
|
ask({ text: data.text });
|
||||||
methods.reset();
|
methods.reset();
|
||||||
|
clearDraft();
|
||||||
},
|
},
|
||||||
[ask, methods],
|
[ask, methods, clearDraft],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
|
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ export default function FileRow({
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
key={index}
|
key={index}
|
||||||
url={file.preview}
|
url={file.preview || file.filepath}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
progress={file.progress}
|
progress={file.progress}
|
||||||
source={file.source}
|
source={file.source}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { SettingsTabValues } from 'librechat-data-provider';
|
||||||
import SendMessageKeyEnter from './EnterToSend';
|
import SendMessageKeyEnter from './EnterToSend';
|
||||||
import ShowCodeSwitch from './ShowCodeSwitch';
|
import ShowCodeSwitch from './ShowCodeSwitch';
|
||||||
import { ForkSettings } from './ForkSettings';
|
import { ForkSettings } from './ForkSettings';
|
||||||
|
import SaveDraft from './SaveDraft';
|
||||||
|
|
||||||
function Messages() {
|
function Messages() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -15,6 +16,9 @@ function Messages() {
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||||
<ShowCodeSwitch />
|
<ShowCodeSwitch />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||||
|
<SaveDraft />
|
||||||
|
</div>
|
||||||
<ForkSettings />
|
<ForkSettings />
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { Switch } from '~/components/ui/Switch';
|
||||||
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
|
export default function SaveDraft({
|
||||||
|
onCheckedChange,
|
||||||
|
}: {
|
||||||
|
onCheckedChange?: (value: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const [saveDrafts, setSaveDrafts] = useRecoilState<boolean>(store.saveDrafts);
|
||||||
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
const handleCheckedChange = (value: boolean) => {
|
||||||
|
setSaveDrafts(value);
|
||||||
|
if (onCheckedChange) {
|
||||||
|
onCheckedChange(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>{localize('com_nav_save_drafts')}</div>
|
||||||
|
<Switch
|
||||||
|
id="saveDrafts"
|
||||||
|
checked={saveDrafts}
|
||||||
|
onCheckedChange={handleCheckedChange}
|
||||||
|
className="ml-4 mt-2"
|
||||||
|
data-testid="saveDrafts"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
213
client/src/hooks/Input/useAutoSave.ts
Normal file
213
client/src/hooks/Input/useAutoSave.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { UseFormSetValue } from 'react-hook-form';
|
||||||
|
import { SetterOrUpdater, useRecoilValue } from 'recoil';
|
||||||
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
|
import { LocalStorageKeys, TFile } from 'librechat-data-provider';
|
||||||
|
import { useGetFiles } from '~/data-provider';
|
||||||
|
import { ExtendedFile } from '~/common';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
|
export const useAutoSave = ({
|
||||||
|
conversationId,
|
||||||
|
textAreaRef,
|
||||||
|
files,
|
||||||
|
setFiles,
|
||||||
|
setValue,
|
||||||
|
}: {
|
||||||
|
conversationId?: string | null;
|
||||||
|
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||||
|
files: Map<string, ExtendedFile>;
|
||||||
|
setFiles: SetterOrUpdater<Map<string, ExtendedFile>>;
|
||||||
|
setValue: UseFormSetValue<{ text: string }>;
|
||||||
|
}) => {
|
||||||
|
// setting for auto-save
|
||||||
|
const saveDrafts = useRecoilValue<boolean>(store.saveDrafts);
|
||||||
|
|
||||||
|
const [currentConversationId, setCurrentConversationId] = useState<string | null>(null);
|
||||||
|
const fileIds = useMemo(() => Array.from(files.keys()), [files]);
|
||||||
|
const { data: fileList } = useGetFiles<TFile[]>();
|
||||||
|
|
||||||
|
const encodeBase64 = (plainText: string): string => {
|
||||||
|
try {
|
||||||
|
const textBytes = new TextEncoder().encode(plainText);
|
||||||
|
return btoa(String.fromCharCode(...textBytes));
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const decodeBase64 = (base64String: string): string => {
|
||||||
|
try {
|
||||||
|
const bytes = atob(base64String);
|
||||||
|
const uint8Array = new Uint8Array(bytes.length);
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
uint8Array[i] = bytes.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return new TextDecoder().decode(uint8Array);
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const restoreFiles = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const filesDraft = JSON.parse(
|
||||||
|
localStorage.getItem(`${LocalStorageKeys.FILES_DRAFT}${id}`) || '[]',
|
||||||
|
) as string[];
|
||||||
|
|
||||||
|
if (filesDraft.length === 0) {
|
||||||
|
setFiles(new Map());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve files stored in localStorage from files in fileList and set them to `setFiles`
|
||||||
|
// If a file is found with `temp_file_id`, use `temp_file_id` as a key in `setFiles`
|
||||||
|
filesDraft.forEach((fileId) => {
|
||||||
|
const fileData = fileList?.find((f) => f.file_id === fileId);
|
||||||
|
const tempFileData = fileList?.find((f) => f.temp_file_id === fileId);
|
||||||
|
const { fileToRecover, fileIdToRecover } = fileData
|
||||||
|
? { fileToRecover: fileData, fileIdToRecover: fileId }
|
||||||
|
: { fileToRecover: tempFileData, fileIdToRecover: tempFileData?.temp_file_id || fileId };
|
||||||
|
|
||||||
|
if (fileToRecover) {
|
||||||
|
setFiles((currentFiles) => {
|
||||||
|
const updatedFiles = new Map(currentFiles);
|
||||||
|
updatedFiles.set(fileIdToRecover, {
|
||||||
|
...fileToRecover,
|
||||||
|
progress: 1,
|
||||||
|
attached: true,
|
||||||
|
size: fileToRecover.bytes,
|
||||||
|
});
|
||||||
|
return updatedFiles;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[fileList, setFiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const restoreText = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const savedDraft = localStorage.getItem(`${LocalStorageKeys.TEXT_DRAFT}${id}`) || '';
|
||||||
|
setValue('text', decodeBase64(savedDraft));
|
||||||
|
},
|
||||||
|
[setValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveText = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
if (!textAreaRef?.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Save the draft of the current conversation before switching
|
||||||
|
if (textAreaRef.current.value === '') {
|
||||||
|
localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${id}`);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(
|
||||||
|
`${LocalStorageKeys.TEXT_DRAFT}${id}`,
|
||||||
|
encodeBase64(textAreaRef.current.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[textAreaRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// This useEffect is responsible for setting up and cleaning up the auto-save functionality
|
||||||
|
// for the text area input. It saves the text to localStorage with a debounce to prevent
|
||||||
|
// excessive writes.
|
||||||
|
if (!saveDrafts || !conversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInput = debounce(() => {
|
||||||
|
if (textAreaRef.current && textAreaRef.current.value) {
|
||||||
|
localStorage.setItem(
|
||||||
|
`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`,
|
||||||
|
encodeBase64(textAreaRef.current.value),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const textArea = textAreaRef.current;
|
||||||
|
if (textArea) {
|
||||||
|
textArea.addEventListener('input', handleInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (textArea) {
|
||||||
|
textArea.removeEventListener('input', handleInput);
|
||||||
|
}
|
||||||
|
handleInput.cancel();
|
||||||
|
};
|
||||||
|
}, [conversationId, saveDrafts, textAreaRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// This useEffect is responsible for saving the current conversation's draft and
|
||||||
|
// restoring the new conversation's draft when switching between conversations.
|
||||||
|
// It handles both text and file drafts, ensuring that the user's input is preserved
|
||||||
|
// across different conversations.
|
||||||
|
|
||||||
|
if (!saveDrafts || !conversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (conversationId === currentConversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear attachment files when switching conversation
|
||||||
|
setFiles(new Map());
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (currentConversationId) {
|
||||||
|
saveText(currentConversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreText(conversationId);
|
||||||
|
restoreFiles(conversationId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentConversationId(conversationId);
|
||||||
|
}, [
|
||||||
|
conversationId,
|
||||||
|
currentConversationId,
|
||||||
|
restoreFiles,
|
||||||
|
restoreText,
|
||||||
|
saveDrafts,
|
||||||
|
saveText,
|
||||||
|
setFiles,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// This useEffect is responsible for saving or removing the current conversation's file drafts
|
||||||
|
// in localStorage whenever the file attachments change.
|
||||||
|
// It ensures that the file drafts are kept up-to-date and can be restored
|
||||||
|
// when the conversation is revisited.
|
||||||
|
|
||||||
|
if (!saveDrafts || !conversationId || currentConversationId !== conversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileIds.length === 0) {
|
||||||
|
localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${conversationId}`);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(
|
||||||
|
`${LocalStorageKeys.FILES_DRAFT}${conversationId}`,
|
||||||
|
JSON.stringify(fileIds),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [files, conversationId, saveDrafts, currentConversationId, fileIds]);
|
||||||
|
|
||||||
|
const clearDraft = useCallback(() => {
|
||||||
|
if (conversationId) {
|
||||||
|
localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`);
|
||||||
|
localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${conversationId}`);
|
||||||
|
}
|
||||||
|
}, [conversationId]);
|
||||||
|
|
||||||
|
return { clearDraft };
|
||||||
|
};
|
||||||
|
|
@ -48,6 +48,7 @@ const useNewConvo = (index = 0) => {
|
||||||
const timeoutIdRef = useRef<NodeJS.Timeout>();
|
const timeoutIdRef = useRef<NodeJS.Timeout>();
|
||||||
const assistantsListMap = useAssistantListMap();
|
const assistantsListMap = useAssistantListMap();
|
||||||
const { pauseGlobalAudio } = usePauseGlobalAudio(index);
|
const { pauseGlobalAudio } = usePauseGlobalAudio(index);
|
||||||
|
const saveDrafts = useRecoilValue<boolean>(store.saveDrafts);
|
||||||
|
|
||||||
const { mutateAsync } = useDeleteFilesMutation({
|
const { mutateAsync } = useDeleteFilesMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -211,14 +212,22 @@ const useNewConvo = (index = 0) => {
|
||||||
setFiles(new Map());
|
setFiles(new Map());
|
||||||
localStorage.setItem(LocalStorageKeys.FILES_TO_DELETE, JSON.stringify({}));
|
localStorage.setItem(LocalStorageKeys.FILES_TO_DELETE, JSON.stringify({}));
|
||||||
|
|
||||||
if (filesToDelete.length > 0) {
|
if (!saveDrafts && filesToDelete.length > 0) {
|
||||||
mutateAsync({ files: filesToDelete });
|
mutateAsync({ files: filesToDelete });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switchToConversation(conversation, preset, modelsData, buildDefault, keepLatestMessage);
|
switchToConversation(conversation, preset, modelsData, buildDefault, keepLatestMessage);
|
||||||
},
|
},
|
||||||
[pauseGlobalAudio, switchToConversation, mutateAsync, setFiles, files, startupConfig],
|
[
|
||||||
|
pauseGlobalAudio,
|
||||||
|
startupConfig,
|
||||||
|
saveDrafts,
|
||||||
|
switchToConversation,
|
||||||
|
files,
|
||||||
|
setFiles,
|
||||||
|
mutateAsync,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -544,6 +544,7 @@ export default {
|
||||||
com_nav_my_files: 'ملفاتي',
|
com_nav_my_files: 'ملفاتي',
|
||||||
com_nav_enter_to_send: 'اضغط على مفتاح الإدخال لإرسال الرسائل',
|
com_nav_enter_to_send: 'اضغط على مفتاح الإدخال لإرسال الرسائل',
|
||||||
com_nav_user_name_display: 'عرض اسم المستخدم في الرسائل',
|
com_nav_user_name_display: 'عرض اسم المستخدم في الرسائل',
|
||||||
|
com_nav_save_drafts: 'حفظ المستخدمين',
|
||||||
com_nav_show_code: 'إظهار الشفرة دائمًا عند استخدام مفسر الشفرة',
|
com_nav_show_code: 'إظهار الشفرة دائمًا عند استخدام مفسر الشفرة',
|
||||||
com_nav_send_message: 'إرسال رسالة',
|
com_nav_send_message: 'إرسال رسالة',
|
||||||
com_nav_setting_beta: 'ميزات تجريبية',
|
com_nav_setting_beta: 'ميزات تجريبية',
|
||||||
|
|
@ -2559,6 +2560,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: 'عرض اسم المستخدم في الرسائل',
|
translated: 'عرض اسم المستخدم في الرسائل',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: 'حفظ المستخدمين محليًا',
|
||||||
|
},
|
||||||
com_nav_show_code: {
|
com_nav_show_code: {
|
||||||
english: 'Always show code when using code interpreter',
|
english: 'Always show code when using code interpreter',
|
||||||
translated: 'إظهار الشفرة دائمًا عند استخدام مفسر الشفرة',
|
translated: 'إظهار الشفرة دائمًا عند استخدام مفسر الشفرة',
|
||||||
|
|
|
||||||
|
|
@ -440,6 +440,7 @@ export default {
|
||||||
com_nav_theme_dark: 'Escuro',
|
com_nav_theme_dark: 'Escuro',
|
||||||
com_nav_theme_light: 'Claro',
|
com_nav_theme_light: 'Claro',
|
||||||
com_nav_user_name_display: 'Exibir nome de usuário nas mensagens',
|
com_nav_user_name_display: 'Exibir nome de usuário nas mensagens',
|
||||||
|
com_nav_save_drafts: 'Salvar rascunhos localmente',
|
||||||
com_nav_show_code: 'Sempre mostrar código ao usar o interpretador de código',
|
com_nav_show_code: 'Sempre mostrar código ao usar o interpretador de código',
|
||||||
com_nav_clear_all_chats: 'Limpar todas as conversas',
|
com_nav_clear_all_chats: 'Limpar todas as conversas',
|
||||||
com_nav_confirm_clear: 'Confirmar Limpeza',
|
com_nav_confirm_clear: 'Confirmar Limpeza',
|
||||||
|
|
@ -2053,6 +2054,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: 'Exibir nome de usuário nas mensagens',
|
translated: 'Exibir nome de usuário nas mensagens',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: 'Salvar rascunhos localmente',
|
||||||
|
},
|
||||||
com_nav_show_code: {
|
com_nav_show_code: {
|
||||||
english: 'Always show code when using code interpreter',
|
english: 'Always show code when using code interpreter',
|
||||||
translated: 'Sempre mostrar código ao usar o interpretador de código',
|
translated: 'Sempre mostrar código ao usar o interpretador de código',
|
||||||
|
|
|
||||||
|
|
@ -454,6 +454,7 @@ export default {
|
||||||
com_nav_theme_dark: 'Dunkel',
|
com_nav_theme_dark: 'Dunkel',
|
||||||
com_nav_theme_light: 'Hell',
|
com_nav_theme_light: 'Hell',
|
||||||
com_nav_user_name_display: 'Benutzernamen in Nachrichten anzeigen',
|
com_nav_user_name_display: 'Benutzernamen in Nachrichten anzeigen',
|
||||||
|
com_nav_save_drafts: 'Entwurf lokal speichern',
|
||||||
com_nav_show_code: 'Code immer anzeigen, wenn Code-Interpreter verwendet wird',
|
com_nav_show_code: 'Code immer anzeigen, wenn Code-Interpreter verwendet wird',
|
||||||
com_nav_clear_all_chats: 'Alle Chats löschen',
|
com_nav_clear_all_chats: 'Alle Chats löschen',
|
||||||
com_nav_confirm_clear: 'Bestätige Löschen',
|
com_nav_confirm_clear: 'Bestätige Löschen',
|
||||||
|
|
@ -2215,6 +2216,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: 'Benutzernamen in Nachrichten anzeigen',
|
translated: 'Benutzernamen in Nachrichten anzeigen',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: 'Entwurf lokal speichern',
|
||||||
|
},
|
||||||
com_nav_show_code: {
|
com_nav_show_code: {
|
||||||
english: 'Always show code when using code interpreter',
|
english: 'Always show code when using code interpreter',
|
||||||
translated: 'Code immer anzeigen, wenn Code-Interpreter verwendet wird',
|
translated: 'Code immer anzeigen, wenn Code-Interpreter verwendet wird',
|
||||||
|
|
|
||||||
|
|
@ -541,6 +541,7 @@ export default {
|
||||||
com_nav_theme_light: 'Light',
|
com_nav_theme_light: 'Light',
|
||||||
com_nav_enter_to_send: 'Press Enter to send messages',
|
com_nav_enter_to_send: 'Press Enter to send messages',
|
||||||
com_nav_user_name_display: 'Display username in messages',
|
com_nav_user_name_display: 'Display username in messages',
|
||||||
|
com_nav_save_drafts: 'Save drafts locally',
|
||||||
com_nav_show_code: 'Always show code when using code interpreter',
|
com_nav_show_code: 'Always show code when using code interpreter',
|
||||||
com_nav_clear_all_chats: 'Clear all chats',
|
com_nav_clear_all_chats: 'Clear all chats',
|
||||||
com_nav_confirm_clear: 'Confirm Clear',
|
com_nav_confirm_clear: 'Confirm Clear',
|
||||||
|
|
|
||||||
|
|
@ -446,6 +446,7 @@ export default {
|
||||||
com_nav_theme_dark: 'Oscuro',
|
com_nav_theme_dark: 'Oscuro',
|
||||||
com_nav_theme_light: 'Claro',
|
com_nav_theme_light: 'Claro',
|
||||||
com_nav_user_name_display: 'Mostrar nombre de usuario en los mensajes',
|
com_nav_user_name_display: 'Mostrar nombre de usuario en los mensajes',
|
||||||
|
com_nav_save_drafts: 'Guardar borradores localmente',
|
||||||
com_nav_show_code: 'Mostrar siempre el código cuando se use el intérprete de código',
|
com_nav_show_code: 'Mostrar siempre el código cuando se use el intérprete de código',
|
||||||
com_nav_clear_all_chats: 'Borrar todos los chats',
|
com_nav_clear_all_chats: 'Borrar todos los chats',
|
||||||
com_nav_confirm_clear: 'Confirmar borrado',
|
com_nav_confirm_clear: 'Confirmar borrado',
|
||||||
|
|
@ -2188,6 +2189,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: 'Mostrar nombre de usuario en los mensajes',
|
translated: 'Mostrar nombre de usuario en los mensajes',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: 'Guardar borradores localmente',
|
||||||
|
},
|
||||||
com_nav_show_code: {
|
com_nav_show_code: {
|
||||||
english: 'Always show code when using code interpreter',
|
english: 'Always show code when using code interpreter',
|
||||||
translated: 'Mostrar siempre el código cuando se use el intérprete de código',
|
translated: 'Mostrar siempre el código cuando se use el intérprete de código',
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,7 @@ export default {
|
||||||
com_nav_theme_dark: 'Sombre',
|
com_nav_theme_dark: 'Sombre',
|
||||||
com_nav_theme_light: 'Clair',
|
com_nav_theme_light: 'Clair',
|
||||||
com_nav_user_name_display: 'Afficher le nom d\'utilisateur dans les messages',
|
com_nav_user_name_display: 'Afficher le nom d\'utilisateur dans les messages',
|
||||||
|
com_nav_save_drafts: 'Enregistrer les brouillons localement',
|
||||||
com_nav_clear_all_chats: 'Effacer toutes les conversations',
|
com_nav_clear_all_chats: 'Effacer toutes les conversations',
|
||||||
com_nav_confirm_clear: 'Confirmer l\'effacement',
|
com_nav_confirm_clear: 'Confirmer l\'effacement',
|
||||||
com_nav_close_sidebar: 'Fermer la barre latérale',
|
com_nav_close_sidebar: 'Fermer la barre latérale',
|
||||||
|
|
@ -1784,6 +1785,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: 'Afficher le nom d\'utilisateur dans les messages',
|
translated: 'Afficher le nom d\'utilisateur dans les messages',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: 'Enregistrer les brouillons localement',
|
||||||
|
},
|
||||||
com_nav_clear_all_chats: {
|
com_nav_clear_all_chats: {
|
||||||
english: 'Clear all chats',
|
english: 'Clear all chats',
|
||||||
translated: 'Effacer toutes les conversations',
|
translated: 'Effacer toutes les conversations',
|
||||||
|
|
|
||||||
|
|
@ -367,6 +367,7 @@ export default {
|
||||||
com_nav_theme_dark: 'כהה',
|
com_nav_theme_dark: 'כהה',
|
||||||
com_nav_theme_light: 'אור',
|
com_nav_theme_light: 'אור',
|
||||||
com_nav_user_name_display: 'הצג שם משתמש בהודעות',
|
com_nav_user_name_display: 'הצג שם משתמש בהודעות',
|
||||||
|
com_nav_save_drafts: 'שמיר את האפצה באותו מחשב',
|
||||||
com_nav_clear_all_chats: 'נקה את כל השיחות',
|
com_nav_clear_all_chats: 'נקה את כל השיחות',
|
||||||
com_nav_confirm_clear: 'אשר נקה',
|
com_nav_confirm_clear: 'אשר נקה',
|
||||||
com_nav_close_sidebar: 'סגור סרגל צד',
|
com_nav_close_sidebar: 'סגור סרגל צד',
|
||||||
|
|
@ -1757,6 +1758,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: 'הצג שם משתמש בהודעות',
|
translated: 'הצג שם משתמש בהודעות',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: 'שמיר את האפצה באותו מחשב',
|
||||||
|
},
|
||||||
com_nav_clear_all_chats: {
|
com_nav_clear_all_chats: {
|
||||||
english: 'Clear all chats',
|
english: 'Clear all chats',
|
||||||
translated: 'נקה את כל השיחות',
|
translated: 'נקה את כל השיחות',
|
||||||
|
|
|
||||||
|
|
@ -326,6 +326,7 @@ export default {
|
||||||
com_nav_theme_dark: 'Gelap',
|
com_nav_theme_dark: 'Gelap',
|
||||||
com_nav_theme_light: 'Terang',
|
com_nav_theme_light: 'Terang',
|
||||||
com_nav_user_name_display: 'Tampilkan nama pengguna dalam pesan',
|
com_nav_user_name_display: 'Tampilkan nama pengguna dalam pesan',
|
||||||
|
com_nav_save_drafts: 'Simpan draft',
|
||||||
com_nav_clear_all_chats: 'Hapus semua obrolan',
|
com_nav_clear_all_chats: 'Hapus semua obrolan',
|
||||||
com_nav_confirm_clear: 'Konfirmasi Hapus',
|
com_nav_confirm_clear: 'Konfirmasi Hapus',
|
||||||
com_nav_close_sidebar: 'Tutup sidebar',
|
com_nav_close_sidebar: 'Tutup sidebar',
|
||||||
|
|
@ -1555,6 +1556,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: 'Tampilkan nama pengguna dalam pesan',
|
translated: 'Tampilkan nama pengguna dalam pesan',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: 'Simpan draft',
|
||||||
|
},
|
||||||
com_nav_clear_all_chats: {
|
com_nav_clear_all_chats: {
|
||||||
english: 'Clear all chats',
|
english: 'Clear all chats',
|
||||||
translated: 'Hapus semua obrolan',
|
translated: 'Hapus semua obrolan',
|
||||||
|
|
|
||||||
|
|
@ -500,6 +500,7 @@ export default {
|
||||||
com_nav_theme_light: 'Chiaro',
|
com_nav_theme_light: 'Chiaro',
|
||||||
com_nav_enter_to_send: 'Premi Invio per inviare messaggi',
|
com_nav_enter_to_send: 'Premi Invio per inviare messaggi',
|
||||||
com_nav_user_name_display: 'Mostra nome utente nei messaggi',
|
com_nav_user_name_display: 'Mostra nome utente nei messaggi',
|
||||||
|
com_nav_save_drafts: 'Salva bozze localmente',
|
||||||
com_nav_show_code: 'Mostra sempre il codice quando si usa l\'interprete di codice',
|
com_nav_show_code: 'Mostra sempre il codice quando si usa l\'interprete di codice',
|
||||||
com_nav_clear_all_chats: 'Cancella tutte le chat',
|
com_nav_clear_all_chats: 'Cancella tutte le chat',
|
||||||
com_nav_confirm_clear: 'Conferma cancellazione',
|
com_nav_confirm_clear: 'Conferma cancellazione',
|
||||||
|
|
@ -2367,6 +2368,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: 'Mostra nome utente nei messaggi',
|
translated: 'Mostra nome utente nei messaggi',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: 'Salva bozze localmente',
|
||||||
|
},
|
||||||
com_nav_show_code: {
|
com_nav_show_code: {
|
||||||
english: 'Always show code when using code interpreter',
|
english: 'Always show code when using code interpreter',
|
||||||
translated: 'Mostra sempre il codice quando si usa l\'interprete di codice',
|
translated: 'Mostra sempre il codice quando si usa l\'interprete di codice',
|
||||||
|
|
|
||||||
|
|
@ -447,6 +447,7 @@ export default {
|
||||||
com_nav_theme_light: 'ライト',
|
com_nav_theme_light: 'ライト',
|
||||||
com_nav_enter_to_send: 'Enterキーでメッセージを送信する',
|
com_nav_enter_to_send: 'Enterキーでメッセージを送信する',
|
||||||
com_nav_user_name_display: 'メッセージにユーザー名を表示する',
|
com_nav_user_name_display: 'メッセージにユーザー名を表示する',
|
||||||
|
com_nav_save_drafts: 'ローカルにドラフトを保存する',
|
||||||
com_nav_show_code: 'Code Interpreter を使用する際は常にコードを表示する',
|
com_nav_show_code: 'Code Interpreter を使用する際は常にコードを表示する',
|
||||||
com_nav_clear_all_chats: 'すべてのチャットを削除する',
|
com_nav_clear_all_chats: 'すべてのチャットを削除する',
|
||||||
com_nav_confirm_clear: '削除を確定',
|
com_nav_confirm_clear: '削除を確定',
|
||||||
|
|
@ -2210,6 +2211,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: 'メッセージにユーザー名を表示する',
|
translated: 'メッセージにユーザー名を表示する',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: 'ローカルにドラフトを保存する',
|
||||||
|
},
|
||||||
com_nav_show_code: {
|
com_nav_show_code: {
|
||||||
english: 'Always show code when using code interpreter',
|
english: 'Always show code when using code interpreter',
|
||||||
translated: 'Code Interpreter を使用する際は常にコードを表示する',
|
translated: 'Code Interpreter を使用する際は常にコードを表示する',
|
||||||
|
|
|
||||||
|
|
@ -538,6 +538,7 @@ export default {
|
||||||
com_nav_my_files: '내 파일',
|
com_nav_my_files: '내 파일',
|
||||||
com_nav_enter_to_send: '엔터키를 눌러 메시지 보내기',
|
com_nav_enter_to_send: '엔터키를 눌러 메시지 보내기',
|
||||||
com_nav_user_name_display: '메시지에서 사용자 이름 표시',
|
com_nav_user_name_display: '메시지에서 사용자 이름 표시',
|
||||||
|
com_nav_save_drafts: '초안을 로컬에 저장',
|
||||||
com_nav_show_code: '코드 인터프리터 사용 시 항상 코드 표시',
|
com_nav_show_code: '코드 인터프리터 사용 시 항상 코드 표시',
|
||||||
com_nav_setting_beta: '베타 기능',
|
com_nav_setting_beta: '베타 기능',
|
||||||
com_nav_setting_account: '계정',
|
com_nav_setting_account: '계정',
|
||||||
|
|
@ -2555,6 +2556,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: '메시지에서 사용자 이름 표시',
|
translated: '메시지에서 사용자 이름 표시',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: '초안을 로컬에 저장',
|
||||||
|
},
|
||||||
com_nav_show_code: {
|
com_nav_show_code: {
|
||||||
english: 'Always show code when using code interpreter',
|
english: 'Always show code when using code interpreter',
|
||||||
translated: '코드 인터프리터 사용 시 항상 코드 표시',
|
translated: '코드 인터프리터 사용 시 항상 코드 표시',
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,7 @@ export default {
|
||||||
com_nav_theme_dark: 'Темная',
|
com_nav_theme_dark: 'Темная',
|
||||||
com_nav_theme_light: 'Светлая',
|
com_nav_theme_light: 'Светлая',
|
||||||
com_nav_user_name_display: 'Отображать имя пользователя в сообщениях',
|
com_nav_user_name_display: 'Отображать имя пользователя в сообщениях',
|
||||||
|
com_nav_save_drafts: 'Сохранить черновики локально',
|
||||||
com_nav_language: 'Локализация',
|
com_nav_language: 'Локализация',
|
||||||
com_nav_setting_account: 'Аккаунт',
|
com_nav_setting_account: 'Аккаунт',
|
||||||
com_nav_profile_picture: 'Изображение профиля',
|
com_nav_profile_picture: 'Изображение профиля',
|
||||||
|
|
@ -1753,6 +1754,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: 'Отображать имя пользователя в сообщениях',
|
translated: 'Отображать имя пользователя в сообщениях',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: 'Сохранить черновики локально',
|
||||||
|
},
|
||||||
com_nav_language: {
|
com_nav_language: {
|
||||||
english: 'Language',
|
english: 'Language',
|
||||||
translated: 'Локализация',
|
translated: 'Локализация',
|
||||||
|
|
|
||||||
|
|
@ -409,6 +409,7 @@ export default {
|
||||||
com_nav_theme_dark: '暗色主题',
|
com_nav_theme_dark: '暗色主题',
|
||||||
com_nav_theme_light: '亮色主题',
|
com_nav_theme_light: '亮色主题',
|
||||||
com_nav_user_name_display: '在消息中显示用户名',
|
com_nav_user_name_display: '在消息中显示用户名',
|
||||||
|
com_nav_save_drafts: '保存草稿本地',
|
||||||
com_nav_show_code: '使用代码解释器时始终显示代码',
|
com_nav_show_code: '使用代码解释器时始终显示代码',
|
||||||
com_nav_clear_all_chats: '清空所有对话',
|
com_nav_clear_all_chats: '清空所有对话',
|
||||||
com_nav_confirm_clear: '确认清空',
|
com_nav_confirm_clear: '确认清空',
|
||||||
|
|
@ -2110,6 +2111,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: '在消息中显示用户名',
|
translated: '在消息中显示用户名',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: '保存草稿本地',
|
||||||
|
},
|
||||||
com_nav_show_code: {
|
com_nav_show_code: {
|
||||||
english: 'Always show code when using code interpreter',
|
english: 'Always show code when using code interpreter',
|
||||||
translated: '使用代码解释器时始终显示代码',
|
translated: '使用代码解释器时始终显示代码',
|
||||||
|
|
|
||||||
|
|
@ -519,6 +519,7 @@ export default {
|
||||||
com_nav_my_files: '我的檔案',
|
com_nav_my_files: '我的檔案',
|
||||||
com_nav_enter_to_send: '按 Enter 鍵傳送訊息',
|
com_nav_enter_to_send: '按 Enter 鍵傳送訊息',
|
||||||
com_nav_user_name_display: '在訊息中顯示使用者名稱',
|
com_nav_user_name_display: '在訊息中顯示使用者名稱',
|
||||||
|
com_nav_save_drafts: '保存草稿本地',
|
||||||
com_nav_show_code: '一律顯示使用程式碼解譯器時的程式碼',
|
com_nav_show_code: '一律顯示使用程式碼解譯器時的程式碼',
|
||||||
com_nav_setting_beta: '測試功能',
|
com_nav_setting_beta: '測試功能',
|
||||||
com_nav_setting_account: '帳號',
|
com_nav_setting_account: '帳號',
|
||||||
|
|
@ -2528,6 +2529,10 @@ export const comparisons = {
|
||||||
english: 'Display username in messages',
|
english: 'Display username in messages',
|
||||||
translated: '在訊息中顯示使用者名稱',
|
translated: '在訊息中顯示使用者名稱',
|
||||||
},
|
},
|
||||||
|
com_nav_save_drafts: {
|
||||||
|
english: 'Save drafts locally',
|
||||||
|
translated: '保存草稿本地',
|
||||||
|
},
|
||||||
com_nav_show_code: {
|
com_nav_show_code: {
|
||||||
english: 'Always show code when using code interpreter',
|
english: 'Always show code when using code interpreter',
|
||||||
translated: '一律顯示使用程式碼解譯器時的程式碼',
|
translated: '一律顯示使用程式碼解譯器時的程式碼',
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ const localStorageAtoms = {
|
||||||
splitAtTarget: atomWithLocalStorage('splitAtTarget', false),
|
splitAtTarget: atomWithLocalStorage('splitAtTarget', false),
|
||||||
rememberForkOption: atomWithLocalStorage('rememberForkOption', true),
|
rememberForkOption: atomWithLocalStorage('rememberForkOption', true),
|
||||||
playbackRate: atomWithLocalStorage<number | null>('playbackRate', null),
|
playbackRate: atomWithLocalStorage<number | null>('playbackRate', null),
|
||||||
|
saveDrafts: atomWithLocalStorage('saveDrafts', false),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { ...staticAtoms, ...localStorageAtoms };
|
export default { ...staticAtoms, ...localStorageAtoms };
|
||||||
|
|
|
||||||
|
|
@ -820,6 +820,10 @@ export enum LocalStorageKeys {
|
||||||
REMEMBER_FORK_OPTION = 'rememberForkOption',
|
REMEMBER_FORK_OPTION = 'rememberForkOption',
|
||||||
/** Key for remembering the split at target fork option modifier */
|
/** Key for remembering the split at target fork option modifier */
|
||||||
FORK_SPLIT_AT_TARGET = 'splitAtTarget',
|
FORK_SPLIT_AT_TARGET = 'splitAtTarget',
|
||||||
|
/** Key for saving text drafts */
|
||||||
|
TEXT_DRAFT = 'textDraft_',
|
||||||
|
/** Key for saving file drafts */
|
||||||
|
FILES_DRAFT = 'filesDraft_',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ForkOptions {
|
export enum ForkOptions {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue