mirror of
https://github.com/coaidev/coai.git
synced 2025-05-30 18:30:32 +09:00
add russian translate, fix delete dialog and add pwa updater
This commit is contained in:
parent
7957b0d47d
commit
2993672af9
@ -41,6 +41,7 @@
|
||||
- 🌏 Internationalization support
|
||||
- 🇨🇳 简体中文
|
||||
- 🇺🇸 English
|
||||
- 🇷🇺 Русский
|
||||
14. 🍎 主题切换
|
||||
- 🍎 Theme switching
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chatnio",
|
||||
"private": false,
|
||||
"version": "2.0.0",
|
||||
"version": "2.6.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@ -44,7 +44,8 @@
|
||||
"sort-by": "^1.2.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"workbox-window": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.5.9",
|
||||
|
6
app/pnpm-lock.yaml
generated
6
app/pnpm-lock.yaml
generated
@ -107,6 +107,9 @@ dependencies:
|
||||
unist-util-visit:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
workbox-window:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
@ -2794,7 +2797,6 @@ packages:
|
||||
|
||||
/@types/trusted-types@2.0.3:
|
||||
resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==}
|
||||
dev: true
|
||||
|
||||
/@types/unist@2.0.8:
|
||||
resolution: {integrity: sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==}
|
||||
@ -7095,7 +7097,6 @@ packages:
|
||||
|
||||
/workbox-core@7.0.0:
|
||||
resolution: {integrity: sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==}
|
||||
dev: true
|
||||
|
||||
/workbox-expiration@7.0.0:
|
||||
resolution: {integrity: sha512-MLK+fogW+pC3IWU9SFE+FRStvDVutwJMR5if1g7oBJx3qwmO69BNoJQVaMXq41R0gg3MzxVfwOGKx3i9P6sOLQ==}
|
||||
@ -7172,7 +7173,6 @@ packages:
|
||||
dependencies:
|
||||
'@types/trusted-types': 2.0.3
|
||||
workbox-core: 7.0.0
|
||||
dev: true
|
||||
|
||||
/wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
@ -69,6 +69,13 @@
|
||||
-webkit-overflow-scrolling: touch;
|
||||
transition: 0.2s ease-in-out;
|
||||
|
||||
.empty {
|
||||
color: hsl(var(--text-secondary));
|
||||
font-size: 14px;
|
||||
margin: auto;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.conversation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -20,18 +20,9 @@ function I18nProvider() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={i18n.language === "cn"}
|
||||
onClick={() => setLanguage(i18n, "cn")}
|
||||
>
|
||||
简体中文
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={i18n.language === "en"}
|
||||
onClick={() => setLanguage(i18n, "en")}
|
||||
>
|
||||
English
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem checked={i18n.language === "cn"} onClick={() => setLanguage(i18n, "cn")}>简体中文</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem checked={i18n.language === "en"} onClick={() => setLanguage(i18n, "en")}>English</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem checked={i18n.language === "ru"} onClick={() => setLanguage(i18n, "ru")}>Русский</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import axios from "axios";
|
||||
|
||||
export const version: string = "2.5.0";
|
||||
export const version: string = "2.6.0";
|
||||
export const deploy: boolean = true;
|
||||
export let rest_api: string = "http://localhost:8094";
|
||||
export let ws_api: string = "ws://localhost:8094";
|
||||
|
149
app/src/i18n.ts
149
app/src/i18n.ts
@ -28,6 +28,7 @@ const resources = {
|
||||
"Request failed. Please check your network and try again.",
|
||||
conversation: {
|
||||
title: "Conversation",
|
||||
"empty": "Empty",
|
||||
"refresh-failed": "Refresh failed",
|
||||
"refresh-failed-prompt":
|
||||
"There was an error during your request. Please try again.",
|
||||
@ -164,6 +165,7 @@ const resources = {
|
||||
"request-failed": "请求失败,请检查您的网络并重试。",
|
||||
conversation: {
|
||||
title: "会话",
|
||||
"empty": "空空如也",
|
||||
"refresh-failed": "刷新失败",
|
||||
"refresh-failed-prompt": "请求出错,请重试。",
|
||||
"remove-title": "是否确定?",
|
||||
@ -273,9 +275,148 @@ const resources = {
|
||||
},
|
||||
},
|
||||
},
|
||||
ru: {
|
||||
translation: {
|
||||
end: "",
|
||||
"not-found": "Страница не найдена",
|
||||
home: "Главная",
|
||||
login: "Войти",
|
||||
logout: "Выйти",
|
||||
quota: "Квота",
|
||||
"try-again": "Попробуйте еще раз",
|
||||
"invalid-token": "Неверный токен",
|
||||
"invalid-token-prompt": "Пожалуйста, попробуйте еще раз.",
|
||||
"login-failed": "Ошибка входа",
|
||||
"login-failed-prompt":
|
||||
"Ошибка входа! Пожалуйста, проверьте срок действия вашего токена и попробуйте еще раз.",
|
||||
"login-success": "Успешный вход",
|
||||
"login-success-prompt": "Вы успешно вошли в систему.",
|
||||
"server-error": "Ошибка сервера",
|
||||
"server-error-prompt":
|
||||
"При входе произошла ошибка. Пожалуйста, попробуйте еще раз.",
|
||||
"request-failed":
|
||||
"Ошибка запроса. Пожалуйста, проверьте свою сеть и попробуйте еще раз.",
|
||||
conversation: {
|
||||
title: "Разговор",
|
||||
"empty": "Пусто",
|
||||
"refresh-failed": "Ошибка обновления",
|
||||
"refresh-failed-prompt":
|
||||
"При выполнении запроса произошла ошибка. Пожалуйста, попробуйте еще раз.",
|
||||
"remove-title": "Вы уверены?",
|
||||
"remove-description":
|
||||
"Это действие нельзя отменить. Это навсегда удалит разговор ",
|
||||
cancel: "Отмена",
|
||||
delete: "Удалить",
|
||||
"delete-success": "Разговор удален",
|
||||
"delete-success-prompt": "Разговор был удален.",
|
||||
"delete-failed": "Ошибка удаления",
|
||||
"delete-failed-prompt":
|
||||
"Не удалось удалить разговор. Пожалуйста, проверьте свою сеть и попробуйте еще раз.",
|
||||
},
|
||||
chat: {
|
||||
web: "веб-поиск",
|
||||
"web-aria": "Переключить веб-поиск",
|
||||
placeholder: "Напишите что-нибудь (/image для генерации изображения)",
|
||||
},
|
||||
message: {
|
||||
copy: "Копировать",
|
||||
save: "Сохранить как файл",
|
||||
use: "Использовать сообщение",
|
||||
},
|
||||
"quota-description": "квота расходов на сообщение",
|
||||
buy: {
|
||||
choose: "Выберите сумму",
|
||||
other: "Другое",
|
||||
"other-desc": "Сколько очков?",
|
||||
buy: "Купить {{amount}} очков",
|
||||
dalle: "Генератор изображений DALL·E",
|
||||
"dalle-free": "5 бесплатных квот в день",
|
||||
gpt4: "GPT-4",
|
||||
flex: "Гибкая тарификация",
|
||||
input: "Вход",
|
||||
output: "Выход",
|
||||
tip: "Цены выровнены (или ниже) по моделям OpenAI",
|
||||
"learn-more": "Узнать больше",
|
||||
"dialog-title": "Купить очки",
|
||||
"dialog-desc": "Вы уверены, что хотите купить {{amount}} очков?",
|
||||
"dialog-cancel": "Отмена",
|
||||
"dialog-buy": "Купить",
|
||||
success: "Покупка прошла успешно",
|
||||
"success-prompt": "Вы успешно приобрели {{amount}} очков.",
|
||||
failed: "Покупка не удалась",
|
||||
"failed-prompt":
|
||||
"Не удалось приобрести очки. Пожалуйста, убедитесь, что у вас достаточно баланса, вы скоро перейдете в кошелек deeptrain для оплаты баланса.",
|
||||
"gpt4-tip": "Совет: функция веб-поиска может потреблять больше входных очков",
|
||||
},
|
||||
pkg: {
|
||||
title: "Пакеты",
|
||||
go: "Перейти к проверке",
|
||||
cert: "Пакет сертификации",
|
||||
"cert-desc":
|
||||
"После сертификации подлинности вы можете получить 50 очков (стоимостью 5 CNY)",
|
||||
teen: "Подростковый пакет",
|
||||
"teen-desc":
|
||||
"После сертификации подлинности подростки (до 18 лет) могут получить дополнительно 150 очков (стоимостью 15 CNY)",
|
||||
close: "Закрыть",
|
||||
state: {
|
||||
true: "Получено",
|
||||
false: "Не получено",
|
||||
},
|
||||
},
|
||||
sub: {
|
||||
title: "Подписка",
|
||||
"dialog-title": "Подписка",
|
||||
free: "Бесплатно",
|
||||
"free-price": "Бесплатно навсегда",
|
||||
pro: "Профессиональный",
|
||||
"pro-price": "8 CNY/месяц",
|
||||
"free-gpt3": "GPT-3.5 бесплатно навсегда",
|
||||
"free-dalle": "5 бесплатных квот в день",
|
||||
"free-web": "веб-поиск",
|
||||
"free-conversation": "хранение разговоров",
|
||||
"free-api": "API вызовы",
|
||||
"pro-gpt4": "GPT-4 10 запросов в день",
|
||||
"pro-dalle": "50 квот в день",
|
||||
"pro-service": "Приоритетная служба поддержки",
|
||||
"pro-thread": "Увеличение параллелизма",
|
||||
current: "Текущая подписка",
|
||||
upgrade: "Обновить",
|
||||
renew: "Продлить",
|
||||
"cannot-select": "Невозможно выбрать",
|
||||
"select-time": "Выберите время подписки",
|
||||
price: "Цена {{price}} CNY",
|
||||
expired: "Ваша подписка Pro истечет через {{expired}} дней",
|
||||
time: {
|
||||
1: "1 месяц",
|
||||
3: "3 месяца",
|
||||
6: "6 месяцев",
|
||||
12: "1 год",
|
||||
},
|
||||
success: "Подписка успешна",
|
||||
"success-prompt": "Вы успешно подписались на {{month}} месяцев Pro.",
|
||||
failed: "Подписка не удалась",
|
||||
"failed-prompt":
|
||||
"Не удалось подписаться, пожалуйста, убедитесь, что у вас достаточно баланса, вы скоро перейдете в кошелек deeptrain для оплаты баланса.",
|
||||
},
|
||||
cancel: "Отмена",
|
||||
confirm: "Подтвердить",
|
||||
percent: "{{cent}}0%",
|
||||
file: {
|
||||
upload: "Загрузить файл",
|
||||
type: "В настоящее время поддерживаются только текстовые файлы для загрузки",
|
||||
drop: "Перетащите файлы сюда или нажмите, чтобы загрузить",
|
||||
"parse-error": "Ошибка разбора",
|
||||
"parse-error-prompt":
|
||||
"Ошибка разбора, в настоящее время поддерживаются только текстовые файлы",
|
||||
"max-length": "Слишком длинный контент",
|
||||
"max-length-prompt":
|
||||
"Содержимое было усечено из-за ограничения длины контекста",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const supportedLanguages = ["en", "cn"];
|
||||
export const supportedLanguages = ["en", "cn", "ru"];
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
@ -296,8 +437,14 @@ export function getStorage(): string {
|
||||
if (storage && supportedLanguages.includes(storage)) {
|
||||
return storage;
|
||||
}
|
||||
// get browser language
|
||||
const lang = navigator.language.split("-")[0];
|
||||
if (supportedLanguages.includes(lang)) {
|
||||
return lang;
|
||||
}
|
||||
return "cn";
|
||||
}
|
||||
|
||||
export function setLanguage(i18n: any, lang: string): void {
|
||||
if (supportedLanguages.includes(lang)) {
|
||||
i18n
|
||||
|
@ -5,6 +5,7 @@ import "./conf.ts";
|
||||
import "./i18n.ts";
|
||||
import "./assets/main.less";
|
||||
import "./assets/globals.less";
|
||||
import "./service.ts";
|
||||
import "./conf.ts";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
|
@ -57,7 +57,6 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "../components/ui/alert-dialog.tsx";
|
||||
import { manager } from "../conversation/manager.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@ -71,6 +70,7 @@ function SideBar() {
|
||||
const open = useSelector((state: RootState) => state.menu.open);
|
||||
const auth = useSelector(selectAuthenticated);
|
||||
const current = useSelector(selectCurrent);
|
||||
const [ removeConversation, setRemoveConversation ] = useState<ConversationInstance | null>(null);
|
||||
const { toast } = useToast();
|
||||
const history: ConversationInstance[] = useSelector(selectHistory);
|
||||
const refresh = useRef(null);
|
||||
@ -116,13 +116,15 @@ function SideBar() {
|
||||
</Button>
|
||||
</div>
|
||||
<div className={`conversation-list`}>
|
||||
{history.map((conversation, i) => (
|
||||
{history.length ? history.map((conversation, i) => (
|
||||
<div
|
||||
className={`conversation ${
|
||||
current === conversation.id ? "active" : ""
|
||||
}`}
|
||||
key={i}
|
||||
onClick={async () => {
|
||||
onClick={async (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.classList.contains("delete") || target.parentElement?.classList.contains("delete")) return;
|
||||
await toggleConversation(dispatch, conversation.id);
|
||||
if (mobile) dispatch(setMenu(false));
|
||||
}}
|
||||
@ -132,56 +134,67 @@ function SideBar() {
|
||||
{filterMessage(conversation.name)}
|
||||
</div>
|
||||
<div className={`id`}>{conversation.id}</div>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Trash2 className={`delete h-4 w-4`} />
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("conversation.remove-title")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("conversation.remove-description")}
|
||||
<strong className={`conversation-name`}>
|
||||
{filterMessage(conversation.name)}
|
||||
</strong>
|
||||
{t("end")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t("conversation.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
if (
|
||||
await deleteConversation(dispatch, conversation.id)
|
||||
)
|
||||
toast({
|
||||
title: t("conversation.delete-success"),
|
||||
description: t(
|
||||
"conversation.delete-success-prompt",
|
||||
),
|
||||
});
|
||||
else
|
||||
toast({
|
||||
title: t("conversation.delete-failed"),
|
||||
description: t(
|
||||
"conversation.delete-failed-prompt",
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("conversation.delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Trash2 className={`delete h-4 w-4`} onClick={
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setRemoveConversation(conversation);
|
||||
}
|
||||
} />
|
||||
</div>
|
||||
))}
|
||||
)) : (
|
||||
<div className={`empty`}>{t("conversation.empty")}</div>
|
||||
)}
|
||||
</div>
|
||||
<AlertDialog open={removeConversation !== null} onOpenChange={(open) => {
|
||||
if (!open) setRemoveConversation(null);
|
||||
}}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("conversation.remove-title")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("conversation.remove-description")}
|
||||
<strong className={`conversation-name`}>
|
||||
{filterMessage(removeConversation?.name || "")}
|
||||
</strong>
|
||||
{t("end")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t("conversation.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (
|
||||
await deleteConversation(dispatch, removeConversation?.id || -1)
|
||||
)
|
||||
toast({
|
||||
title: t("conversation.delete-success"),
|
||||
description: t(
|
||||
"conversation.delete-success-prompt",
|
||||
),
|
||||
});
|
||||
else
|
||||
toast({
|
||||
title: t("conversation.delete-failed"),
|
||||
description: t(
|
||||
"conversation.delete-failed-prompt",
|
||||
),
|
||||
});
|
||||
setRemoveConversation(null);
|
||||
}}
|
||||
>
|
||||
{t("conversation.delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
) : (
|
||||
<Button className={`login-action`} variant={`default`} onClick={login}>
|
||||
|
17
app/src/service.ts
Normal file
17
app/src/service.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// @ts-ignore
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
|
||||
export const updateSW = registerSW({
|
||||
onRegisteredSW(url: string, registration: ServiceWorkerRegistration) {
|
||||
if (!(!registration.installing && navigator)) return
|
||||
if (('connection' in navigator) && !navigator.onLine) return
|
||||
|
||||
fetch(url, { headers: { 'Service-Worker': 'script', 'Cache-Control': 'no-cache' }, cache: 'no-store' })
|
||||
.then(async (resp) => {
|
||||
if (resp?.status === 200) {
|
||||
await registration.update();
|
||||
if (registration.onupdatefound) console.log('update found');
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user