This commit is contained in:
JordiMa 2024-01-15 11:23:11 -06:00 committed by GitHub
commit 451150cffc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 828 additions and 212 deletions

View file

@ -18,8 +18,8 @@
"@types/react-dom": "^18.0.11",
"buffer": "^5.7.1",
"parcel": "^2.8.3",
"path-browserify": "^1.0.1",
"parcel-reporter-static-files-copy": "^1.5.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10"
},
"dependencies": {
@ -32,8 +32,8 @@
"@tabler/icons-react": "^2.9.0",
"@tanstack/react-location": "^3.7.4",
"@types/node": "18.15.0",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"dexie": "^3.2.3",
"dexie-export-import": "^4.0.6",
"dexie-react-hooks": "^1.1.3",
@ -41,6 +41,8 @@
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"gpt-token-utils": "^1.2.0",
"i18next": "^22.4.15",
"i18next-browser-languagedetector": "^7.0.1",
"lodash": "^4.17.21",
"nanoid": "^4.0.1",
"next": "13.2.4",
@ -48,6 +50,7 @@
"openai-ext": "^1.2.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.2.2",
"react-icons": "^4.8.0",
"react-markdown": "^8.0.6",
"remark-gfm": "^3.0.1",

157
src/assets/i18n/en.json Normal file
View file

@ -0,0 +1,157 @@
{
"translation": {
"notAnotherUI": "Not just another ChatGPT user-interface!",
"changeOpenAIKey": "Change OpenAI Key",
"enterOpenAIKey": "Enter OpenAI Key",
"downloadDesktopApp": "Download Desktop App",
"freeAndOpenSource": {
"title": "Free and open source",
"description": "This app is provided for free and the source code is available on GitHub."
},
"privacyFocused": {
"title": "Privacy focused",
"description": "No tracking, no cookies, no bullshit. All your data is stored locally."
},
"bestExperience": {
"title": "Best experience",
"description": "Crafted with love and care to provide the best experience possible."
},
"search": "Search",
"lightMode": "Light Mode",
"darkMode": "Dark Mode",
"settings": "Settings",
"database": "Database",
"sourceCode": "Source Code",
"followOnTwitter": "Follow on Twitter",
"giveFeedback": "Give Feedback",
"chats": "Chats",
"newChat": "New Chat",
"prompts": "Prompts",
"newPrompt": "New Prompt",
"savePrompt": "Save Prompt",
"createPrompt": "Create Prompt",
"savedPrompt": {
"title": "Saved",
"message": "Prompt created"
},
"error": {
"title": "Error"
},
"networkError": {
"title": "Error",
"message": "No internet connection."
},
"chatIdError": {
"title": "Error",
"message": "chatId is not defined. Please create a chat to get started."
},
"apiKeyError": {
"title": "Error",
"message": "OpenAI API Key is not defined. Please set your API Key"
},
"title": "Title",
"content": "Content",
"save": "save",
"youAre": "You are",
"respondInTone": "Respond in",
"respondInStyle": "Respond in",
"tone": "tone",
"style": "style",
"youAreChatGPT": "You are ChatGPT, a large language model trained by OpenAI.",
"defineTitleChat": "What would be a short and relevant title for this chat ? You must strictly answer with only the title, no other text is allowed.",
"character": "Character",
"toneList": "Tone",
"styleList": "Style",
"format": "Format",
"yourMessageHere": "Your message here...",
"deleteChats": "Delete Chats",
"confirmDeleteChats": "Are you sure you want to delete your chats?",
"deleteChat": "Delete Chat",
"confirmDeleteChat": "Are you sure you want to delete this chat?",
"delete": "Delete",
"deleted": "Deleted",
"chatDeleted": "Chat deleted.",
"removeChatError": {
"title": "Error",
"message": "Can't remove chat. Please refresh the page and try again."
},
"openAIAPIKey": "OpenAI API Key",
"getYourOpenAIAPIkey": "Get your OpenAI API key",
"APIKeyStoredLocally": "The API Key is stored locally on your browser and never sent anywhere else.",
"openAIType": "OpenAI Type",
"openAIKeySaved": {
"title": "saved",
"message": "Your OpenAI Key has been saved."
},
"openAITypeSaved": {
"title": "saved",
"message": "Your OpenAI Type has been saved."
},
"openIATTypeCustom": "Custom (e.g. Azure OpenAI)",
"openAIModel": "OpenAI Model (OpenAI Only)",
"openAIModelSaved": {
"title": "saved",
"message": "Your OpenAI Model has been saved."
},
"warning": "Warning",
"warningCostMessage": "The displayed cost was not updated yet to reflect the costs for each model. Right now it will always show the cost for GPT-3.5 on OpenAI.",
"openAIAuth": "OpenAI Auth (Custom Only)",
"openAIAuthSaved": {
"title": "saved",
"message": "Your OpenAI Auth has been saved."
},
"none": "None",
"bearerToken": "Bearer Token",
"apiKey": "API Key",
"openAIBaseSaved": {
"title": "saved",
"message": "Your OpenAI Base has been saved."
},
"openAIAPIBase": "OpenAI API Base (Custom Only)",
"openAIAPIVersion": "OpenAI API Version (Custom Only)",
"openAIVersionSaved": {
"title": "saved",
"message": "Your OpenAI Version has been saved."
},
"newChatFromPrompt": "New Chat From Prompt",
"copied": "Copied",
"copy": "Copy",
"words": "words",
"editPrompt": "Edit Prompt",
"promptUpdated": {
"title": "saved",
"message": "Prompt updated"
},
"editChat": "Edit Chat",
"chatUpdated": {
"title": "saved",
"message": "Chat updated"
},
"name": "Name",
"deleteAllData": "Delete All Data",
"confirmDeleteAllData": "Are you sure you want to delete your data?",
"messages": "Messages",
"exportingData" : {
"title": "Exporting Data",
"message": "Your data is being exported."
},
"exportData": "Export Data",
"importingData" : {
"title": "Import Data",
"message": "Your data is being imported."
},
"importData": "Import Data",
"selectedFileError": {
"title": "Error",
"message": "The file you selected is invalid"
},
"edit": "Edit",
"promptDeleted": "Prompt deleted.",
"removePromptError": {
"title": "Error",
"message": "Can't remove prompt. Please refresh the page and try again."
},
"deletePrompt": "Delete Prompt",
"confirmDeletePrompt": "Are you sure you want to delete this prompt?"
}
}

157
src/assets/i18n/fr.json Normal file
View file

@ -0,0 +1,157 @@
{
"translation": {
"notAnotherUI": "Ce n'est pas juste une autre interface de ChatGPT !",
"changeOpenAIKey": "Modifier la clé API d'OpenAI",
"enterOpenAIKey": "Entrer la clé API d'OpenAI",
"downloadDesktopApp": "Télécharger l'application de bureau",
"freeAndOpenSource": {
"title": "Gratuit et open source",
"description": "Cette application est gratuite et le code source est disponible sur GitHub."
},
"privacyFocused": {
"title": "Centré sur la vie privée",
"description": "Pas de suivi, pas de cookies, pas de mensonge. Toutes vos données sont stockées localement."
},
"bestExperience": {
"title": "La meilleure expérience",
"description": "Fabriqué avec amour et soin pour offrir la meilleure expérience possible."
},
"search": "Rechercher",
"lightMode": "Mode clair",
"darkMode": "Mode sombre",
"settings": "Paramètres",
"database": "Base de données",
"sourceCode": "Code source",
"followOnTwitter": "Suivez-nous sur Twitter",
"giveFeedback": "Donnez votre avis",
"chats": "Conversations",
"newChat": "Nouvelle conversation",
"prompts": "Prompts",
"newPrompt": "Nouveau Prompt",
"savePrompt": "Enregistrer le Prompt",
"createPrompt": "Créer un Prompt",
"savedPrompt": {
"title": "Sauvegardé",
"message": "Prompt créée"
},
"error": {
"title": "Erreur"
},
"networkError": {
"title": "Erreur",
"message": "Pas de connexion internet."
},
"chatIdError": {
"title": "Erreur",
"message": "le ChatId n'est pas défini. Veuillez créer une conversation pour commencer."
},
"apiKeyError": {
"title": "Erreur",
"message": "La clé API OpenAI n'est pas définie. Veuillez définir votre clé API"
},
"title": "Titre",
"content": "Contenu",
"save": "Enregistrer",
"youAre": "Tu es",
"respondInTone": "Réponds sur un ton",
"respondInStyle": "Réponds dans un style",
"tone": "",
"style": "",
"youAreChatGPT": "Tu es ChatGPT, un modèle de langage avancé entraîné par OpenAI.",
"defineTitleChat": "Titre court et pertinent pour cette discussion ? Vous devez répondre strictement avec le titre, aucun autre texte n'est autorisé.",
"character": "Personnage",
"toneList": "Ton",
"styleList": "Style",
"format": "Format",
"yourMessageHere": "Votre message ici...",
"deleteChats": "Supprimer les conversations",
"confirmDeleteChats": "Êtes-vous sûr de vouloir supprimer les conversations ?",
"deleteChat": "Supprimer la conversation",
"confirmDeleteChat": "Êtes-vous sûr de vouloir supprimer la conversation ?",
"delete": "Supprimer",
"deleted": "Supprimée",
"chatDeleted": "Conversation supprimée.",
"removeChatError": {
"title": "Erreur",
"message": "Impossible de supprimer la conversation. Veuillez actualiser la page et réessayer."
},
"openAIAPIKey": "Clé API OpenAI",
"getYourOpenAIAPIkey": "Obtenez votre clé API OpenAI",
"APIKeyStoredLocally": "La clé API est stockée localement sur votre navigateur et n'est jamais envoyée ailleurs.",
"openAIType": "Type d'OpenAI",
"openAIKeySaved": {
"title": "Sauvegardé",
"message": "Votre clé OpenAI a été enregistrée."
},
"openAITypeSaved": {
"title": "Sauvegardé",
"message": "Votre type d'OpenAI a été enregistré."
},
"openIATTypeCustom": "Personnalisé (par exemple: Azure OpenAI)",
"openAIModel": "Modèle OpenAI (OpenAI seulement)",
"openAIModelSaved": {
"title": "Sauvegardé",
"message": "Votre modèle OpenAI a été enregistré."
},
"warning": "Avertissement",
"warningCostMessage": "Le coût affiché n'a pas encore été mis à jour pour refléter les coûts de chaque modèle. Pour l'instant, le coût affiché est toujours celui du modèle GPT-3.5 de l'OpenAI.",
"openAIAuth": "Methode d'authentification OpenAI (personnalisé seulement)",
"openAIAuthSaved": {
"title": "Sauvegardé",
"message": "Votre methode d'authentification OpenAI a été enregistré."
},
"none": "Aucune",
"bearerToken": "Bearer Token",
"apiKey": "Clé API",
"openAIBaseSaved": {
"title": "Sauvegardé",
"message": "Votre URL d'API OpenAI a été enregistrée."
},
"openAIAPIBase": "URL d'API OpenAI (personnalisé uniquement)",
"openAIAPIVersion": "Version de l'API OpenAI (personnalisé uniquement)",
"openAIVersionSaved": {
"title": "Sauvegardé",
"message": "Votre version OpenAI a été enregistrée."
},
"newChatFromPrompt": "Nouvelle conversattion à partir du prompt",
"copied": "Copié !",
"copy": "Copier",
"words": "mots",
"editPrompt": "Modifier le prompt",
"promptUpdated": {
"title": "Sauvegardé",
"message": "Prompt enregistré"
},
"editChat": "Modifier la conversation",
"chatUpdated": {
"title": "Sauvegardé",
"message": "Conversation enregistrée"
},
"name": "Nom",
"deleteAllData": "Supprimer toutes les données",
"confirmDeleteAllData": "Êtes-vous sûr de vouloir effacer toutes vos données ?",
"messages": "Messages",
"exportingData" : {
"title": "Exportation de données",
"message": "Vos données sont en cours d'exportation."
},
"exportData": "Exporter les données",
"importingData" : {
"title": "Importation de données",
"message": "Vos données sont en cours d'importation."
},
"importData": "Importer les données",
"selectedFileError": {
"title": "Erreur",
"message": "Le fichier sélectionné n'est pas valide"
},
"edit": "Modifier",
"promptDeleted": "Prompt supprimée.",
"removePromptError": {
"title": "Erreur",
"message": "Impossible de supprimer le prompt. Veuillez rafraîchir la page et réessayer."
},
"deletePrompt": "Supprimer le prompt",
"confirmDeletePrompt": "Êtes-vous sûr de vouloir supprimer ce prompt ?"
}
}

View file

@ -13,6 +13,7 @@ import {
import { ChatRoute } from "../routes/ChatRoute";
import { IndexRoute } from "../routes/IndexRoute";
import { Layout } from "./Layout";
import '../i18n';
const history = createHashHistory();
const location = new ReactLocation({ history });
@ -30,7 +31,6 @@ export function App() {
setColorScheme(value || (colorScheme === "dark" ? "light" : "dark"));
useHotkeys([["mod+J", () => toggleColorScheme()]]);
return (
<Router
location={location}

View file

@ -8,6 +8,7 @@ import { useChatId } from "../hooks/useChatId";
import { DeleteChatModal } from "./DeleteChatModal";
import { EditChatModal } from "./EditChatModal";
import { MainLink } from "./MainLink";
import {t} from "i18next";
export function Chats({ search }: { search: string }) {
const chatId = useChatId();
@ -55,10 +56,10 @@ export function Chats({ search }: { search: string }) {
</Menu.Target>
<Menu.Dropdown>
<EditChatModal chat={chat}>
<Menu.Item>Edit</Menu.Item>
<Menu.Item>{t('edit')}</Menu.Item>
</EditChatModal>
<DeleteChatModal chat={chat}>
<Menu.Item>Delete</Menu.Item>
<Menu.Item>{t('delete')}</Menu.Item>
</DeleteChatModal>
</Menu.Dropdown>
</Menu>

View file

@ -13,6 +13,7 @@ import { IconPlaylistAdd, IconPlus } from "@tabler/icons-react";
import { nanoid } from "nanoid";
import { useEffect, useState } from "react";
import { db } from "../db";
import {t} from "i18next";
export function CreatePromptModal({ content }: { content?: string }) {
const [opened, { open, close }] = useDisclosure(false);
@ -27,17 +28,17 @@ export function CreatePromptModal({ content }: { content?: string }) {
return (
<>
{content ? (
<Tooltip label="Save Prompt" position="left">
<Tooltip label={t('savePrompt')} position="left">
<ActionIcon onClick={open}>
<IconPlaylistAdd opacity={0.5} size={20} />
</ActionIcon>
</Tooltip>
) : (
<Button fullWidth onClick={open} leftIcon={<IconPlus size={20} />}>
New Prompt
{t('newPrompt')}
</Button>
)}
<Modal opened={opened} onClose={close} title="Create Prompt" size="lg">
<Modal opened={opened} onClose={close} title={t('createPrompt')} size="lg">
<form
onSubmit={async (event) => {
try {
@ -51,22 +52,22 @@ export function CreatePromptModal({ content }: { content?: string }) {
createdAt: new Date(),
});
notifications.show({
title: "Saved",
message: "Prompt created",
title: t('savedPrompt.title'),
message: t('savedPrompt.message'),
});
close();
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -78,14 +79,14 @@ export function CreatePromptModal({ content }: { content?: string }) {
>
<Stack>
<TextInput
label="Title"
label={t('title')}
value={title}
onChange={(event) => setTitle(event.currentTarget.value)}
formNoValidate
data-autofocus
/>
<Textarea
placeholder="Content"
placeholder={t('content')}
autosize
minRows={5}
maxRows={10}
@ -93,7 +94,7 @@ export function CreatePromptModal({ content }: { content?: string }) {
onChange={(event) => setValue(event.currentTarget.value)}
/>
<Button type="submit" loading={submitting}>
Save
{t('save')}
</Button>
</Stack>
</form>

View file

@ -8,6 +8,7 @@ import { cloneElement, ReactElement } from "react";
import { db } from "../db";
import { DeleteAllDataModal } from "./DeleteAllDataModal";
import { DeleteChatsModal } from "./DeleteChatsModal";
import {t} from "i18next";
export function DatabaseModal({ children }: { children: ReactElement }) {
const [opened, { open, close }] = useDisclosure(false);
@ -27,7 +28,7 @@ export function DatabaseModal({ children }: { children: ReactElement }) {
<Modal
opened={opened}
onClose={close}
title="Database"
title={t('database')}
size="lg"
withinPortal
keepMounted
@ -45,7 +46,7 @@ export function DatabaseModal({ children }: { children: ReactElement }) {
tt="uppercase"
fw={700}
>
Chats
{t('chats')}
</Text>
</Card>
<Card withBorder sx={{ flex: 1, marginLeft: -1 }}>
@ -59,7 +60,7 @@ export function DatabaseModal({ children }: { children: ReactElement }) {
tt="uppercase"
fw={700}
>
Messages
{t('messages')}
</Text>
</Card>
<Card withBorder sx={{ flex: 1, marginLeft: -1 }}>
@ -73,7 +74,7 @@ export function DatabaseModal({ children }: { children: ReactElement }) {
tt="uppercase"
fw={700}
>
Prompts
{t('prompts')}
</Text>
</Card>
</Flex>
@ -89,12 +90,12 @@ export function DatabaseModal({ children }: { children: ReactElement }) {
"application/json"
);
notifications.show({
title: "Exporting Data",
message: "Your data is being exported.",
title: t('exportingData.title'),
message: t('exportingData.message'),
});
}}
>
Export Data
{t('exportData')}
</Button>
<input
id="file-upload-btn"
@ -110,15 +111,15 @@ export function DatabaseModal({ children }: { children: ReactElement }) {
})
.then(() => {
notifications.show({
title: "Importing data",
message: "Your data is being imported.",
title: t('importingData.title'),
message: t('importingData.message'),
});
})
.catch((error) => {
notifications.show({
title: "Error",
title: t('selectedFileError.title'),
color: "red",
message: "The file you selected is invalid",
message: t('selectedFileError.message'),
});
});
}}
@ -131,7 +132,7 @@ export function DatabaseModal({ children }: { children: ReactElement }) {
variant="default"
leftIcon={<IconDatabaseImport size={20} />}
>
Import Data
{t('importData')}
</Button>
</Group>
<Group>

View file

@ -2,6 +2,7 @@ import { Button, Modal, Stack, Text } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconTrash } from "@tabler/icons-react";
import { db } from "../db";
import {t} from "i18next";
export function DeleteAllDataModal({ onOpen }: { onOpen: () => void }) {
const [opened, { open, close }] = useDisclosure(false, { onOpen });
@ -14,17 +15,17 @@ export function DeleteAllDataModal({ onOpen }: { onOpen: () => void }) {
color="red"
leftIcon={<IconTrash size={20} />}
>
Delete All Data
{t('deleteAllData')}
</Button>
<Modal
opened={opened}
onClose={close}
title="Delete All Data"
title={t('deleteAllData')}
size="md"
withinPortal
>
<Stack>
<Text size="sm">Are you sure you want to delete your data?</Text>
<Text size="sm">{t('confirmDeleteAllData')}</Text>
<Button
onClick={async () => {
await db.delete();
@ -33,7 +34,7 @@ export function DeleteAllDataModal({ onOpen }: { onOpen: () => void }) {
}}
color="red"
>
Delete
{t('delete')}
</Button>
</Stack>
</Modal>

View file

@ -6,6 +6,7 @@ import { cloneElement, ReactElement, useEffect, useState } from "react";
import { Chat, db } from "../db";
import { useApiKey } from "../hooks/useApiKey";
import { useChatId } from "../hooks/useChatId";
import {t} from "i18next";
export function DeleteChatModal({
chat,
@ -29,7 +30,7 @@ export function DeleteChatModal({
return (
<>
{cloneElement(children, { onClick: open })}
<Modal opened={opened} onClose={close} title="Delete Chat">
<Modal opened={opened} onClose={close} title={t('deleteChat')}>
<form
onSubmit={async (event) => {
try {
@ -43,22 +44,21 @@ export function DeleteChatModal({
close();
notifications.show({
title: "Deleted",
message: "Chat deleted.",
title: t('deleted'),
message: t('chatDeleted'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
} else {
notifications.show({
title: "Error",
title: t('removeChatError.title'),
color: "red",
message:
"Can't remove chat. Please refresh the page and try again.",
message: t('removeChatError.message'),
});
}
} finally {
@ -67,9 +67,9 @@ export function DeleteChatModal({
}}
>
<Stack>
<Text size="sm">Are you sure you want to delete this chat?</Text>
<Text size="sm">{t('confirmDeleteChat')}</Text>
<Button type="submit" color="red" loading={submitting}>
Delete
{t('delete')}
</Button>
</Stack>
</form>

View file

@ -2,6 +2,7 @@ import { Button, Modal, Stack, Text } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconTrash } from "@tabler/icons-react";
import { db } from "../db";
import {t} from "i18next";
export function DeleteChatsModal({ onOpen }: { onOpen: () => void }) {
const [opened, { open, close }] = useDisclosure(false, { onOpen });
@ -14,17 +15,17 @@ export function DeleteChatsModal({ onOpen }: { onOpen: () => void }) {
color="red"
leftIcon={<IconTrash size={20} />}
>
Delete Chats
{t('deleteChats')}
</Button>
<Modal
opened={opened}
onClose={close}
title="Delete Chats"
title={t('deleteChats')}
size="md"
withinPortal
>
<Stack>
<Text size="sm">Are you sure you want to delete your chats?</Text>
<Text size="sm">{t('confirmDeleteChats')}</Text>
<Button
onClick={async () => {
await db.chats.clear();
@ -34,7 +35,7 @@ export function DeleteChatsModal({ onOpen }: { onOpen: () => void }) {
}}
color="red"
>
Delete
{t('delete')}
</Button>
</Stack>
</Modal>

View file

@ -7,6 +7,8 @@ import { useEffect, useState } from "react";
import { db, Prompt } from "../db";
import { useApiKey } from "../hooks/useApiKey";
import { useChatId } from "../hooks/useChatId";
import {t} from "i18next";
import * as tty from "tty";
export function DeletePromptModal({ prompt }: { prompt: Prompt }) {
const [opened, { open, close }] = useDisclosure(false);
@ -23,7 +25,7 @@ export function DeletePromptModal({ prompt }: { prompt: Prompt }) {
return (
<>
<Modal opened={opened} onClose={close} title="Delete Prompt" size="md">
<Modal opened={opened} onClose={close} title={t('deletePrompt')} size="md">
<form
onSubmit={async (event) => {
try {
@ -33,22 +35,21 @@ export function DeletePromptModal({ prompt }: { prompt: Prompt }) {
close();
notifications.show({
title: "Deleted",
message: "Chat deleted.",
title: t('deleted'),
message: t('promptDeleted'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
} else {
notifications.show({
title: "Error",
title: t('removePromptError.title'),
color: "red",
message:
"Can't remove chat. Please refresh the page and try again.",
message: t('removePromptError.message'),
});
}
} finally {
@ -57,14 +58,14 @@ export function DeletePromptModal({ prompt }: { prompt: Prompt }) {
}}
>
<Stack>
<Text size="sm">Are you sure you want to delete this prompt?</Text>
<Text size="sm">{t('confirmDeletePrompt')}</Text>
<Button type="submit" color="red" loading={submitting}>
Delete
{t('delete')}
</Button>
</Stack>
</form>
</Modal>
<Tooltip label="Delete Prompt">
<Tooltip label={t('deletePrompt')}>
<ActionIcon color="red" size="lg" onClick={open}>
<IconTrash size={20} />
</ActionIcon>

View file

@ -3,6 +3,7 @@ import { useDisclosure } from "@mantine/hooks";
import { notifications } from "@mantine/notifications";
import { cloneElement, ReactElement, useEffect, useState } from "react";
import { Chat, db } from "../db";
import {t} from "i18next";
export function EditChatModal({
chat,
@ -22,7 +23,7 @@ export function EditChatModal({
return (
<>
{cloneElement(children, { onClick: open })}
<Modal opened={opened} onClose={close} title="Edit Chat" withinPortal>
<Modal opened={opened} onClose={close} title={t('editChat')} withinPortal>
<form
onSubmit={async (event) => {
try {
@ -32,22 +33,22 @@ export function EditChatModal({
chat.description = value;
});
notifications.show({
title: "Saved",
message: "",
title: t('chatUpdated.title'),
message: t('chatUpdated.message'),
});
close();
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -59,14 +60,14 @@ export function EditChatModal({
>
<Stack>
<TextInput
label="Name"
label={t('name')}
value={value}
onChange={(event) => setValue(event.currentTarget.value)}
formNoValidate
data-autofocus
/>
<Button type="submit" loading={submitting}>
Save
{t('save')}
</Button>
</Stack>
</form>

View file

@ -12,6 +12,7 @@ import { notifications } from "@mantine/notifications";
import { IconPencil } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import { db, Prompt } from "../db";
import {t} from "i18next";
export function EditPromptModal({ prompt }: { prompt: Prompt }) {
const [opened, { open, close }] = useDisclosure(false);
@ -26,7 +27,7 @@ export function EditPromptModal({ prompt }: { prompt: Prompt }) {
return (
<>
<Modal opened={opened} onClose={close} title="Edit Prompt" size="lg">
<Modal opened={opened} onClose={close} title={t('editPrompt')} size="lg">
<form
onSubmit={async (event) => {
try {
@ -37,21 +38,21 @@ export function EditPromptModal({ prompt }: { prompt: Prompt }) {
chat.content = value;
});
notifications.show({
title: "Saved",
message: "Prompt updated",
title: t('promptUpdated.title'),
message: t('promptUpdated.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -63,14 +64,14 @@ export function EditPromptModal({ prompt }: { prompt: Prompt }) {
>
<Stack>
<TextInput
label="Title"
label={t('title')}
value={title}
onChange={(event) => setTitle(event.currentTarget.value)}
formNoValidate
data-autofocus
/>
<Textarea
label="Content"
label={t('content')}
autosize
minRows={5}
maxRows={10}
@ -78,12 +79,12 @@ export function EditPromptModal({ prompt }: { prompt: Prompt }) {
onChange={(event) => setValue(event.currentTarget.value)}
/>
<Button type="submit" loading={submitting}>
Save
{t('save')}
</Button>
</Stack>
</form>
</Modal>
<Tooltip label="Edit Prompt">
<Tooltip label={t('editPrompt')}>
<ActionIcon size="lg" onClick={open}>
<IconPencil size={20} />
</ActionIcon>

View file

@ -41,6 +41,7 @@ import { LogoText } from "./Logo";
import { Prompts } from "./Prompts";
import { SettingsModal } from "./SettingsModal";
import { config } from "../utils/config";
import {t} from "i18next";
declare global {
interface Window {
@ -51,7 +52,8 @@ declare global {
export function Layout() {
const theme = useMantineTheme();
const [opened, setOpened] = useState(false);
const [tab, setTab] = useState<"Chats" | "Prompts">("Chats");
// @ts-ignore
const [tab, setTab] = useState<"Chats" | "Prompts">(t('chats'));
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const navigate = useNavigate();
const router = useRouter();
@ -126,10 +128,10 @@ export function Layout() {
fullWidth
value={tab}
onChange={(value) => setTab(value as typeof tab)}
data={["Chats", "Prompts"]}
data={[t('chats'), t('prompts')]}
/>
<Box sx={{ padding: 4 }}>
{tab === "Chats" && (
{tab === t('chats') && (
<Button
fullWidth
leftIcon={<IconPlus size={20} />}
@ -137,17 +139,17 @@ export function Layout() {
const id = nanoid();
db.chats.add({
id,
description: "New Chat",
description: t('newChat'),
totalTokens: 0,
createdAt: new Date(),
});
navigate({ to: `/chats/${id}` });
}}
>
New Chat
{t('newChat')}
</Button>
)}
{tab === "Prompts" && <CreatePromptModal />}
{tab === t('prompts') && <CreatePromptModal />}
</Box>
</Navbar.Section>
<Navbar.Section
@ -163,7 +165,7 @@ export function Layout() {
<TextInput
variant="unstyled"
radius={0}
placeholder="Search"
placeholder={t('search')}
value={search}
onChange={(event) =>
setSearch(event.currentTarget.value.toLowerCase())
@ -180,8 +182,8 @@ export function Layout() {
/>
</Navbar.Section>
<Navbar.Section grow component={ScrollArea}>
{tab === "Chats" && <Chats search={search} />}
{tab === "Prompts" && (
{tab === t('chats') && <Chats search={search} />}
{tab === t('prompts') && (
<Prompts search={search} onPlay={() => setTab("Chats")} />
)}
</Navbar.Section>
@ -189,7 +191,7 @@ export function Layout() {
<Center>
{config.allowDarkModeToggle && (
<Tooltip
label={colorScheme === "dark" ? "Light Mode" : "Dark Mode"}
label={colorScheme === "dark" ? t('lightMode') : t('darkMode')}
>
<ActionIcon
sx={{ flex: 1 }}
@ -206,7 +208,7 @@ export function Layout() {
)}
{config.allowSettingsModal && (
<SettingsModal>
<Tooltip label="Settings">
<Tooltip label={t('settings')}>
<ActionIcon sx={{ flex: 1 }} size="xl">
<IconSettings size={20} />
</ActionIcon>
@ -215,14 +217,14 @@ export function Layout() {
)}
{config.allowDatabaseModal && (
<DatabaseModal>
<Tooltip label="Database">
<Tooltip label={t('database')}>
<ActionIcon sx={{ flex: 1 }} size="xl">
<IconDatabase size={20} />
</ActionIcon>
</Tooltip>
</DatabaseModal>
)}
<Tooltip label="Source Code">
<Tooltip label={t('sourceCode')}>
<ActionIcon
component="a"
href="https://github.com/deiucanta/chatpad"
@ -234,7 +236,7 @@ export function Layout() {
</ActionIcon>
</Tooltip>
{config.showTwitterLink && (
<Tooltip label="Follow on Twitter">
<Tooltip label={t('followOnTwitter')}>
<ActionIcon
component="a"
href="https://twitter.com/deiucanta"
@ -247,7 +249,7 @@ export function Layout() {
</Tooltip>
)}
{config.showFeedbackLink && (
<Tooltip label="Give Feedback">
<Tooltip label={t('giveFeedback')}>
<ActionIcon
component="a"
href="https://feedback.chatpad.ai"

View file

@ -20,6 +20,7 @@ import "../styles/markdown.scss";
import { CreatePromptModal } from "./CreatePromptModal";
import { LogoIcon } from "./Logo";
import { ScrollIntoView } from "./ScrollIntoView";
import {t} from "i18next";
export function MessageItem({ message }: { message: Message }) {
const clipboard = useClipboard({ timeout: 500 });
@ -55,7 +56,7 @@ export function MessageItem({ message }: { message: Message }) {
<CopyButton value={String(props.children)}>
{({ copied, copy }) => (
<Tooltip
label={copied ? "Copied" : "Copy"}
label={copied ? t('copied') : t('copy')}
position="left"
>
<ActionIcon
@ -74,7 +75,7 @@ export function MessageItem({ message }: { message: Message }) {
{message.role === "assistant" && (
<Box>
<Text size="sm" color="dimmed">
{wordCount} words
{wordCount} {t('words')}
</Text>
</Box>
)}
@ -83,18 +84,13 @@ export function MessageItem({ message }: { message: Message }) {
<CreatePromptModal content={message.content} />
<CopyButton value={message.content}>
{({ copied, copy }) => (
<Tooltip label={copied ? "Copied" : "Copy"} position="left">
<Tooltip label={copied ? t('copied') : t('copy')} position="left">
<ActionIcon onClick={copy}>
<IconCopy opacity={0.5} size={20} />
</ActionIcon>
</Tooltip>
)}
</CopyButton>
{/* <Tooltip label={`${wordCount} words`} position="left">
<ActionIcon>
<IconInfoCircle opacity={0.5} size={20} />
</ActionIcon>
</Tooltip> */}
</Box>
</Flex>
</Card>

View file

@ -8,6 +8,7 @@ import { db } from "../db";
import { createChatCompletion } from "../utils/openai";
import { DeletePromptModal } from "./DeletePromptModal";
import { EditPromptModal } from "./EditPromptModal";
import {t} from "i18next";
export function Prompts({
onPlay,
@ -80,7 +81,7 @@ export function Prompts({
</Text>
</Box>
<Group spacing="none">
<Tooltip label="New Chat From Prompt">
<Tooltip label={t('newChatFromPrompt')}>
<ActionIcon
size="lg"
onClick={async () => {
@ -88,7 +89,7 @@ export function Prompts({
const id = nanoid();
await db.chats.add({
id,
description: "New Chat",
description: t('newChat'),
totalTokens: 0,
createdAt: new Date(),
});
@ -105,8 +106,7 @@ export function Prompts({
const result = await createChatCompletion(apiKey, [
{
role: "system",
content:
"You are ChatGPT, a large language model trained by OpenAI.",
content: t('youAreChatGPT'),
},
{ role: "user", content: prompt.content },
]);

View file

@ -18,6 +18,7 @@ import { cloneElement, ReactElement, useEffect, useState } from "react";
import { db } from "../db";
import { config } from "../utils/config";
import { checkOpenAIKey } from "../utils/openai";
import {t} from "i18next";
export function SettingsModal({ children }: { children: ReactElement }) {
const [opened, { open, close }] = useDisclosure(false);
@ -58,7 +59,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
return (
<>
{cloneElement(children, { onClick: open })}
<Modal opened={opened} onClose={close} title="Settings" size="lg">
<Modal opened={opened} onClose={close} title={t('settings')} size="lg">
<Stack>
<form
onSubmit={async (event) => {
@ -71,21 +72,21 @@ export function SettingsModal({ children }: { children: ReactElement }) {
console.log(apiKey);
});
notifications.show({
title: "Saved",
message: "Your OpenAI Key has been saved.",
title: t('openAIKeySaved.title'),
message: t('openAIKeySaved.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -97,7 +98,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
>
<Flex gap="xs" align="end">
<PasswordInput
label="OpenAI API Key"
label={t('openAIAPIKey')}
placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
sx={{ flex: 1 }}
value={value}
@ -105,7 +106,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
formNoValidate
/>
<Button type="submit" loading={submitting}>
Save
{t('save')}
</Button>
</Flex>
</form>
@ -116,19 +117,18 @@ export function SettingsModal({ children }: { children: ReactElement }) {
href="https://platform.openai.com/account/api-keys"
target="_blank"
>
Get your OpenAI API key
{t('getYourOpenAIAPIkey')}
</Anchor>
</Text>
</List.Item>
<List.Item>
<Text size="sm" color="dimmed">
The API Key is stored locally on your browser and never sent
anywhere else.
{t('APIKeyStoredLocally')}
</Text>
</List.Item>
</List>
<Select
label="OpenAI Type"
label={t('openAIType')}
value={type}
onChange={async (value) => {
setSubmitting(true);
@ -137,21 +137,21 @@ export function SettingsModal({ children }: { children: ReactElement }) {
openAiApiType: value ?? 'openai',
});
notifications.show({
title: "Saved",
message: "Your OpenAI Type has been saved.",
title: t('openAIKeySaved.title'),
message: t('openAIKeySaved.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -161,10 +161,10 @@ export function SettingsModal({ children }: { children: ReactElement }) {
}
}}
withinPortal
data={[{ "value": "openai", "label": "OpenAI"}, { "value": "custom", "label": "Custom (e.g. Azure OpenAI)"}]}
data={[{ "value": "openai", "label": "OpenAI"}, { "value": "custom", "label": t('openIATTypeCustom')}]}
/>
<Select
label="OpenAI Model (OpenAI Only)"
label={t('openAIModel')}
value={model}
onChange={async (value) => {
setSubmitting(true);
@ -173,21 +173,21 @@ export function SettingsModal({ children }: { children: ReactElement }) {
openAiModel: value ?? undefined,
});
notifications.show({
title: "Saved",
message: "Your OpenAI Model has been saved.",
title: t('openAIModelSaved.title'),
message: t('openAIModelSaved.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -199,12 +199,11 @@ export function SettingsModal({ children }: { children: ReactElement }) {
withinPortal
data={config.availableModels}
/>
<Alert color="orange" title="Warning">
The displayed cost was not updated yet to reflect the costs for each
model. Right now it will always show the cost for GPT-3.5 on OpenAI.
<Alert color="orange" title={t('warning')}>
{t('warningCostMessage')}
</Alert>
<Select
label="OpenAI Auth (Custom Only)"
label={t('openAIAuth')}
value={auth}
onChange={async (value) => {
setSubmitting(true);
@ -213,21 +212,21 @@ export function SettingsModal({ children }: { children: ReactElement }) {
openAiApiAuth: value ?? 'none',
});
notifications.show({
title: "Saved",
message: "Your OpenAI Auth has been saved.",
title: t('openAIModelSaved.title'),
message: t('openAIModelSaved.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -237,7 +236,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
}
}}
withinPortal
data={[{ "value": "none", "label": "None"}, { "value": "bearer-token", "label": "Bearer Token"}, { "value": "api-key", "label": "API Key"}]}
data={[{ "value": "none", "label": t('none')}, { "value": "bearer-token", "label": t('bearerToken')}, { "value": "api-key", "label": t('apiKey')}]}
/>
<form
onSubmit={async (event) => {
@ -249,21 +248,21 @@ export function SettingsModal({ children }: { children: ReactElement }) {
console.log(row);
});
notifications.show({
title: "Saved",
message: "Your OpenAI Base has been saved.",
title: t('openAIBaseSaved.title'),
message: t('openAIBaseSaved.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -275,7 +274,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
>
<Flex gap="xs" align="end">
<TextInput
label="OpenAI API Base (Custom Only)"
label={t('openAIAPIBase')}
placeholder="https://<resource-name>.openai.azure.com/openai/deployments/<deployment>"
sx={{ flex: 1 }}
value={base}
@ -283,7 +282,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
formNoValidate
/>
<Button type="submit" loading={submitting}>
Save
{t('save')}
</Button>
</Flex>
</form>
@ -297,21 +296,21 @@ export function SettingsModal({ children }: { children: ReactElement }) {
console.log(row);
});
notifications.show({
title: "Saved",
message: "Your OpenAI Version has been saved.",
title: t('openAIVersionSaved.title'),
message: t('openAIVersionSaved.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -323,7 +322,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
>
<Flex gap="xs" align="end">
<TextInput
label="OpenAI API Version (Custom Only)"
label={t('openAIAPIVersion')}
placeholder="2023-03-15-preview"
sx={{ flex: 1 }}
value={version}
@ -331,7 +330,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
formNoValidate
/>
<Button type="submit" loading={submitting}>
Save
{t('save')}
</Button>
</Flex>
</form>

22
src/i18n.tsx Normal file
View file

@ -0,0 +1,22 @@
import i18n from "i18next";
import {initReactI18next} from "react-i18next";
import LanguageDetector from 'i18next-browser-languagedetector';
import * as en from './assets/i18n/en.json';
import * as fr from './assets/i18n/fr.json';
export const resources = {
en,
fr
};
i18n
.use(initReactI18next)
.use(LanguageDetector)
.init({
resources,
fallbackLng: "en",
interpolation: {
escapeValue: false
}
});
export default i18n;

11
src/react-i18next.d.tsx Normal file
View file

@ -0,0 +1,11 @@
// import the original type declarations
import 'react-i18next';
// import all namespaces (for the default language, only)
import { resources, defaultNS } from './i18n';
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: typeof defaultNS;
resources: typeof resources['en'];
}
}

View file

@ -24,6 +24,7 @@ import {
createChatCompletion,
createStreamChatCompletion,
} from "../utils/openai";
import {t} from "i18next";
export function ChatRoute() {
const chatId = useChatId();
@ -55,14 +56,13 @@ export function ChatRoute() {
const getSystemMessage = () => {
const message: string[] = [];
if (writingCharacter) message.push(`You are ${writingCharacter}.`);
if (writingTone) message.push(`Respond in ${writingTone} tone.`);
if (writingStyle) message.push(`Respond in ${writingStyle} style.`);
// @ts-ignore
if (writingCharacter) message.push(t('youAre') + ` ${writingCharacter}.`);
if (writingTone) message.push(t('respondInTone') + ` ${writingTone} ` + t('tone'));
if (writingStyle) message.push(t('respondInStyle') +` ${writingStyle} ` + t('style'));
if (writingFormat) message.push(writingFormat);
if (message.length === 0)
message.push(
"You are ChatGPT, a large language model trained by OpenAI."
);
message.push(t('youAreChatGPT'));
return message.join(" ");
};
@ -71,18 +71,18 @@ export function ChatRoute() {
if (!chatId) {
notifications.show({
title: "Error",
title: t('chatIdError.title'),
color: "red",
message: "chatId is not defined. Please create a chat to get started.",
message: t('chatIdError.message'),
});
return;
}
if (!apiKey) {
notifications.show({
title: "Error",
title: t('apiKeyError.title'),
color: "red",
message: "OpenAI API Key is not defined. Please set your API Key",
message: t('apiKeyError.message'),
});
return;
}
@ -127,7 +127,7 @@ export function ChatRoute() {
setSubmitting(false);
if (chat?.description === "New Chat") {
if (chat?.description === t('newChat')) {
const messages = await db.messages
.where({ chatId })
.sortBy("createdAt");
@ -142,8 +142,7 @@ export function ChatRoute() {
})),
{
role: "user",
content:
"What would be a short and relevant title for this chat ? You must strictly answer with only the title, no other text is allowed.",
content: t('defineTitleChat'),
},
]);
const chatDescription =
@ -151,7 +150,7 @@ export function ChatRoute() {
if (createChatDescription.data.usage) {
await db.chats.where({ id: chatId }).modify((chat) => {
chat.description = chatDescription ?? "New Chat";
chat.description = chatDescription ?? t('newChat');
if (chat.totalTokens) {
chat.totalTokens +=
createChatDescription.data.usage!.total_tokens;
@ -164,15 +163,15 @@ export function ChatRoute() {
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -264,7 +263,7 @@ export function ChatRoute() {
value={writingCharacter}
onChange={setWritingCharacter}
data={config.writingCharacters}
placeholder="Character"
placeholder={t('character')}
variant="filled"
searchable
clearable
@ -274,7 +273,7 @@ export function ChatRoute() {
value={writingTone}
onChange={setWritingTone}
data={config.writingTones}
placeholder="Tone"
placeholder={t('toneList')}
variant="filled"
searchable
clearable
@ -284,7 +283,7 @@ export function ChatRoute() {
value={writingStyle}
onChange={setWritingStyle}
data={config.writingStyles}
placeholder="Style"
placeholder={t('styleList')}
variant="filled"
searchable
clearable
@ -294,7 +293,7 @@ export function ChatRoute() {
value={writingFormat}
onChange={setWritingFormat}
data={config.writingFormats}
placeholder="Format"
placeholder={t('format')}
variant="filled"
searchable
clearable
@ -306,7 +305,7 @@ export function ChatRoute() {
<Textarea
key={chatId}
sx={{ flex: 1 }}
placeholder="Your message here..."
placeholder={t('yourMessageHere')}
autosize
autoFocus
disabled={submitting}

View file

@ -20,11 +20,30 @@ import { Logo } from "../components/Logo";
import { SettingsModal } from "../components/SettingsModal";
import { db } from "../db";
import { config } from "../utils/config";
import {t} from "i18next";
export function IndexRoute() {
const settings = useLiveQuery(() => db.settings.get("general"));
const { openAiApiKey } = settings ?? {};
const features = [
{
icon: IconCurrencyDollar,
title: t('freeAndOpenSource.title'),
description: t('freeAndOpenSource.description'),
},
{
icon: IconLock,
title: t('privacyFocused.title'),
description: t('privacyFocused.description'),
},
{
icon: IconNorthStar,
title: t('bestExperience.title'),
description: t('bestExperience.description'),
},
];
return (
<>
<Center py="xl" sx={{ height: "100%" }}>
@ -34,7 +53,7 @@ export function IndexRoute() {
<Logo style={{ maxWidth: 240 }} />
</Text>
<Text mt={4} size="xl">
Not just another ChatGPT user-interface!
{t('notAnotherUI')}
</Text>
<SimpleGrid
mt={50}
@ -42,8 +61,8 @@ export function IndexRoute() {
spacing={30}
breakpoints={[{ maxWidth: "md", cols: 1 }]}
>
{features.map((feature) => (
<div key={feature.title}>
{features.map((feature, index) => (
<div key={index}>
<ThemeIcon variant="outline" size={50} radius={50}>
<feature.icon size={26} stroke={1.5} />
</ThemeIcon>
@ -64,7 +83,7 @@ export function IndexRoute() {
variant={openAiApiKey ? "light" : "filled"}
leftIcon={<IconKey size={20} />}
>
{openAiApiKey ? "Change OpenAI Key" : "Enter OpenAI Key"}
{openAiApiKey ? t('changeOpenAIKey') : t('enterOpenAIKey')}
</Button>
</SettingsModal>
)}
@ -77,7 +96,7 @@ export function IndexRoute() {
variant="outline"
leftIcon={<IconCloudDownload size={20} />}
>
Download Desktop App
{t('downloadDesktopApp')}
</Button>
)}
</Group>
@ -85,25 +104,4 @@ export function IndexRoute() {
</Center>
</>
);
}
const features = [
{
icon: IconCurrencyDollar,
title: "Free and open source",
description:
"This app is provided for free and the source code is available on GitHub.",
},
{
icon: IconLock,
title: "Privacy focused",
description:
"No tracking, no cookies, no bullshit. All your data is stored locally.",
},
{
icon: IconNorthStar,
title: "Best experience",
description:
"Crafted with love and care to provide the best experience possible.",
},
];
}

View file

@ -1,5 +1,5 @@
{
"defaultModel": "gpt-3.5-turbo",
"defaultModel": "gpt-3.5-turbo",
"defaultType": "openai",
"defaultAuth": "api-key",
"defaultBase": "",

227
src/static/config_fr.json Normal file
View file

@ -0,0 +1,227 @@
{
"defaultModel": "gpt-3.5-turbo",
"defaultType": "openai",
"defaultAuth": "api-key",
"defaultBase": "",
"defaultVersion": "",
"defaultKey": "",
"availableModels": [
{
"value": "gpt-3.5-turbo",
"label": "GPT-3.5-TURBO (ChatGPT par défaut)"
},
{
"value": "gpt-3.5-turbo-0613",
"label": "GPT-3.5-TURBO-0613"
},
{
"value": "gpt-3.5-turbo-0301",
"label": "GPT-3.5-TURBO-0301 (Legacy)"
},
{
"value": "gpt-3.5-turbo-16k",
"label": "GPT-3.5-TURBO-16K"
},
{
"value": "gpt-3.5-turbo-16k-0613",
"label": "GPT-3.5-TURBO-16K-0613"
},
{
"value": "gpt-4",
"label": "GPT-4 (Bêta limitée)"
},
{
"value": "gpt-4-0613",
"label": "GPT-4-0613 (Bêta limitée)"
},
{
"value": "gpt-4-0314",
"label": "GPT-4-0314 (Bêta limitée, Legacy)"
},
{
"value": "gpt-4-32k",
"label": "GPT-4-32K (Bêta limitée)"
},
{
"value": "gpt-4-32k-0613",
"label": "GPT-4-32K-0613 (Bêta limitée)"
},
{
"value": "gpt-4-32k-0314",
"label": "GPT-4-32K-0314 (Bêta limitée, Legacy)"
}
],
"writingCharacters": [
{
"label": "Humoriste",
"value": "Un artiste de spectacle qui divertit son public en racontant des blagues et des histoires humoristiques."
},
{
"label": "Coach de vie",
"value": "Un professionnel qui aide les individus à identifier et atteindre leurs objectifs personnels et professionnels."
},
{
"label": "Conseiller en orientation",
"value": "Un professionnel qui aide les individus à explorer et à choisir des carrières, à développer des stratégies de recherche d'emploi et à améliorer leur performance professionnelle."
},
{
"label": "Nutritionniste",
"value": "Un professionnel de la santé qui se spécialise dans l'étude de la nutrition et de ses effets sur le corps."
},
{
"label": "Chef de produit",
"value": "Un professionnel qui supervise le développement et la commercialisation des produits d'une entreprise."
},
{
"label": "Entraîneur personnel",
"value": "Un professionnel de la remise en forme qui travaille avec des individus pour élaborer des programmes d'exercices personnalisés et améliorer leur santé et leur condition physique globales."
},
{
"label": "Expert en astuces de vie",
"value": "Une personne qui utilise des méthodes non conventionnelles pour résoudre des problèmes et augmenter la productivité dans la vie quotidienne."
},
{
"label": "Conseiller en voyages",
"value": "Un professionnel qui aide les individus à planifier et réserver des arrangements de voyage."
},
{
"label": "Coach pleine conscience",
"value": "Un professionnel qui aide les individus à développer des pratiques de pleine conscience pour réduire le stress et améliorer le bien-être."
},
{
"label": "Conseiller financier",
"value": "Un professionnel qui fournit des conseils et des orientations sur la planification financière, les stratégies d'investissement et la planification de la retraite."
},
{
"label": "Tuteur linguistiques",
"value": "Un enseignant qui aide les individus à apprendre et à améliorer leurs compétences linguistiques."
},
{
"label": "Guide touristique",
"value": "Un professionnel qui guide les visites et fournit des informations sur les attractions locales et la culture."
},
{
"label": "Expert marketing",
"value": "Un professionnel qui développe et met en oeuvre des stratégies de marketing pour promouvoir des produits et services."
},
{
"label": "Développeur logiciels",
"value": "Un professionnel qui conçoit, développe et maintient des applications et des systèmes informatiques."
},
{
"label": "Coach relations amoureuses",
"value": "Un professionnel qui aide les individus à améliorer leurs compétences en matière de rencontres et de relations amoureuses."
},
{
"label": "Expert en bricolage",
"value": "Une personne qui est habile à réaliser une large gamme de projets de bricolage autour de la maison."
},
{
"label": "Journaliste",
"value": "Un professionnel qui enquête et rapporte sur les événements et les nouvelles en cours."
},
{
"label": "Écrivain technique",
"value": "Un professionnel qui écrit sur la technologie et les sujets connexes pour une variété d'audiences."
},
{
"label": "Chef cuisinier",
"value": "Un professionnel de la cuisine qui prépare des repas et gère les opérations de la cuisine."
},
{
"label": "Vendeur professionnel",
"value": "Un professionnel qui vend des produits et services aux entreprises et aux consommateurs."
},
{
"label": "Avocat startup technologique",
"value": "Un professionnel du droit qui se spécialise dans la fourniture de conseils et de services juridiques aux entreprises technologiques en démarrage."
},
{
"label": "Designer graphique",
"value": "Un professionnel qui conçoit des supports visuels tels que des logos, des brochures et des sites web."
},
{
"label": "Chercheur universitaire",
"value": "Un professionnel qui mène des recherches et produit des travaux universitaires dans un domaine universitaire particulier."
},
{
"label": "Soutien à la clientèle",
"value": "Un professionnel qui fournit une assistance et un soutien aux clients ayant des questions ou des problèmes avec les produits ou services d'une entreprise."
},
{
"label": "Consultant RH",
"value": "Un professionnel qui fournit des conseils et des orientations aux organisations en matière de gestion et de stratégie des ressources humaines."
}
],
"writingTones": [
"Affirmé",
"Autoritaire",
"Détendu",
"Confiant",
"Condescendant",
"Conversationnel",
"Diplomatique",
"Direct",
"Éloquent",
"Formel",
"Amical",
"Humoristique",
"Informatif",
"Inspirant",
"Intense",
"Irritable",
"Blagueur",
"Polie",
"Sarcastique",
"Sincère",
"Apaisant",
"Sévère",
"Compassionnel",
"Tactique",
"Spirituel"
],
"writingStyles": [
"Académique",
"Analytique",
"Argumentatif",
"Conversationnel",
"Créatif",
"Critique",
"Descriptif",
"Explicatif",
"Informatif",
"Instructif",
"Investigatif",
"Journalistique",
"Métaphorique",
"Narratif",
"Persuasif",
"Poétique",
"Satirique",
"Technique"
],
"writingFormats": [
{
"value": "Réponds de manière concise autant que possible",
"label": "Concis"
},
{
"value": "Réfléchir étape par étape",
"label": "Étape par étape"
},
{
"value": "Réponds avec un niveau de détail minutieux",
"label": "Détailler"
},
{
"value": "Explique comme si j'avais cinq ans",
"label": "Explique comme si j'avais 5 ans"
}
],
"showDownloadLink": true,
"allowDarkModeToggle": true,
"allowSettingsModal": true,
"allowDatabaseModal": true,
"showTwitterLink": true,
"showFeedbackLink": true
}

View file

@ -1,3 +1,5 @@
import i18n from "i18next";
interface Config {
defaultModel: AvailableModel["value"];
defaultType: 'openai' | 'custom';
@ -36,7 +38,7 @@ interface WritingFormat {
export let config: Config;
export const loadConfig = async () => {
const response = await fetch("config.json");
const response = await fetch("config_"+i18n.resolvedLanguage+".json");
config = await response.json();
return config;
};

View file

@ -5,6 +5,7 @@
"esModuleInterop": true,
"lib": ["es2018", "dom"],
"strict": true,
"skipLibCheck": true
"skipLibCheck": true,
"resolveJsonModule": true
}
}

View file

@ -35,7 +35,7 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7":
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.3", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7":
version "7.21.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==
@ -1273,10 +1273,10 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/react-dom@18.0.11":
version "18.0.11"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33"
integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==
"@types/react-dom@^18.0.11":
version "18.2.7"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.7.tgz#67222a08c0a6ae0a0da33c3532348277c70abb63"
integrity sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==
dependencies:
"@types/react" "*"
@ -1289,10 +1289,10 @@
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@18.0.28":
version "18.0.28"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065"
integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==
"@types/react@^18.0.28":
version "18.2.15"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.15.tgz#14792b35df676c20ec3cf595b262f8c615a73066"
integrity sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@ -2741,6 +2741,13 @@ html-dom-parser@1.2.0:
domhandler "4.3.1"
htmlparser2 "7.2.0"
html-parse-stringify@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
dependencies:
void-elements "3.1.0"
html-react-parser@1.4.12:
version "1.4.12"
resolved "https://registry.yarnpkg.com/html-react-parser/-/html-react-parser-1.4.12.tgz#5d4336e3853a75e4ac678c9815c15c58581bb30e"
@ -2791,6 +2798,20 @@ human-signals@^4.3.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2"
integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==
i18next-browser-languagedetector@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz#ead34592edc96c6c3a618a51cb57ad027c5b5d87"
integrity sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==
dependencies:
"@babel/runtime" "^7.19.4"
i18next@^22.4.15:
version "22.4.15"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-22.4.15.tgz#951882b751872994f8502b5a6ef6f796e6a7d7f8"
integrity sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==
dependencies:
"@babel/runtime" "^7.20.6"
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@ -4232,6 +4253,14 @@ react-error-overlay@6.0.9:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
react-i18next@^12.2.2:
version "12.2.2"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-12.2.2.tgz#38a6fad11acf4f2abfc5611bdb6b1918d0f47578"
integrity sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg==
dependencies:
"@babel/runtime" "^7.20.6"
html-parse-stringify "^3.0.1"
react-icons@^4.8.0:
version "4.8.0"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.8.0.tgz#621e900caa23b912f737e41be57f27f6b2bff445"
@ -5036,6 +5065,11 @@ vfile@^5.0.0:
unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0"
void-elements@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
weak-lru-cache@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19"