Improve: use database to save user settings (#1041)

* add user settings

* fix user setting

* migrate dict settings

* migrate hotkeys

* fix hotkey setting

* update library settings

* migrate gpt Engine

* use user setting key enum

* migrate openai

* migrate more settings

* migrate whisper config

* migrate whisper

* refactor

* clean up

* migrate profile

* migrate recorder config

* refactor

* refactor

* fix e2e

* add api status

* fix e2e

* fix app init

* fetch apiUrl before fetch user

* update stt engine enums

* update enums

* update enums

* refactor login flow

* Fix warning

* Login from remembered users

* fix e2e

* refactor

* add unauthorized alert

* feat: 🎸 dict import update (#1040)

* rectified. according to Issues.

* issue #1025

* feat: add Vietnamese language to support (#1043)

* feat: add vietnamese language to support

* fix: update Vietnamese language name to native form

---------

Co-authored-by: Ryan <trongdv@coccoc.com>

* upgrade deps

* update locales

---------

Co-authored-by: divisey <18656007202@163.com>
Co-authored-by: xiaolai <lixiaolai@gmail.com>
Co-authored-by: ryan <69750456+ryangwn@users.noreply.github.com>
Co-authored-by: Ryan <trongdv@coccoc.com>
This commit is contained in:
an-lee
2024-09-06 18:32:09 +08:00
committed by GitHub
parent fef9a9459b
commit fffb97f8dc
67 changed files with 2185 additions and 1050 deletions

View File

@@ -7,11 +7,11 @@
"markdown-it-mathjax3": "^4.3.2",
"markdown-it-sub": "^2.0.0",
"markdown-it-sup": "^2.0.0",
"mermaid": "^11.0.2",
"sass": "^1.77.8",
"mermaid": "^11.1.0",
"sass": "^1.78.0",
"vitepress": "^1.3.4",
"vitepress-plugin-mermaid": "^2.0.16",
"vue": "^3.4.38"
"vue": "^3.5.3"
},
"scripts": {
"dev": "vitepress dev",

View File

@@ -10,16 +10,16 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxtjs/seo": "^2.0.0-rc.19",
"nuxt": "^3.13.0",
"nuxt-og-image": "^3.0.0-rc.65",
"vue": "^3.4.38",
"@nuxtjs/seo": "^2.0.0-rc.21",
"nuxt": "^3.13.1",
"nuxt-og-image": "^3.0.0-rc.66",
"vue": "^3.5.3",
"vue-router": "^4.4.3"
},
"devDependencies": {
"autoprefixer": "^10.4.20",
"postcss": "^8.4.43",
"sass": "^1.77.8",
"postcss": "^8.4.45",
"sass": "^1.78.0",
"tailwindcss": "^3.4.10"
}
}

View File

@@ -66,9 +66,6 @@ test("validate whisper command", async () => {
});
console.info(res.log);
expect(res.success).toBeTruthy();
const settings = fs.readJsonSync(path.join(resultDir, "settings.json"));
expect(settings.whisper.service).toBe("azure");
});
test("valid ffmpeg command", async () => {
@@ -83,9 +80,6 @@ test("validate echogarden align command", async () => {
return window.__ENJOY_APP__.echogarden.check();
});
expect(res).toBeTruthy();
const settings = fs.readJsonSync(path.join(resultDir, "settings.json"));
expect(settings.whisper.service).toBe("azure");
});
test("should setup default library path", async () => {

View File

@@ -61,13 +61,13 @@ test.describe("with login", async () => {
settings.user = user;
fs.writeJsonSync(path.join(resultDir, "settings.json"), settings);
page.route("**/api/me", (route) => {
await page.route("**/api/me", (route) => {
route.fulfill({
json: user,
});
});
page.route("**/api/stories", (route) => {
await page.route("**/api/stories", (route) => {
route.fulfill({
json: {
stories: [],
@@ -199,6 +199,7 @@ test.describe("with login", async () => {
// add to library
await page.getByTestId("message-start-shadow").click();
await page.getByTestId("transcribe-continue-button").click();
await page.getByTestId("audio-player").waitFor();
await page
.getByTestId("media-player-container")

View File

@@ -41,7 +41,7 @@
"@electron-forge/plugin-vite": "^7.4.0",
"@electron-forge/publisher-github": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@playwright/test": "^1.46.1",
"@playwright/test": "^1.47.0",
"@tailwindcss/typography": "^0.5.15",
"@types/ahoy.js": "^0.4.2",
"@types/autosize": "^4.0.3",
@@ -53,7 +53,7 @@
"@types/lodash": "^4.17.7",
"@types/mark.js": "^8.11.12",
"@types/mustache": "^4.2.5",
"@types/node": "^22.5.2",
"@types/node": "^22.5.4",
"@types/prop-types": "^15.7.12",
"@types/rails__actioncable": "^6.1.11",
"@types/react": "^18.3.5",
@@ -61,16 +61,16 @@
"@types/unzipper": "^0.10.10",
"@types/validator": "^13.12.1",
"@types/wavesurfer.js": "^6.0.12",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^8.4.0",
"@typescript-eslint/parser": "^8.4.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"electron": "^32.0.1",
"electron": "^32.0.2",
"electron-devtools-installer": "^3.2.0",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.9.1",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-import": "^2.30.0",
"flora-colossus": "^2.0.0",
"octokit": "^4.0.2",
"progress": "^2.0.3",
@@ -82,7 +82,7 @@
"ts-node": "^10.9.2",
"tslib": "^2.7.0",
"typescript": "^5.5.4",
"vite": "^5.4.2",
"vite": "^5.4.3",
"vite-plugin-static-copy": "^1.0.6",
"zx": "^8.1.5"
},
@@ -90,7 +90,7 @@
"@andrkrn/ffprobe-static": "^5.2.0",
"@electron-forge/publisher-s3": "^7.4.0",
"@hookform/resolvers": "^3.9.0",
"@langchain/community": "^0.2.31",
"@langchain/community": "^0.2.32",
"@mozilla/readability": "^0.5.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
@@ -141,7 +141,7 @@
"decamelize-keys": "^2.0.1",
"echogarden": "^1.5.0",
"electron-context-menu": "^4.0.4",
"electron-log": "^5.1.7",
"electron-log": "^5.2.0",
"electron-settings": "^4.0.4",
"electron-squirrel-startup": "^1.0.1",
"ffmpeg-static": "^5.2.0",
@@ -150,19 +150,19 @@
"html-to-text": "^9.0.5",
"https-proxy-agent": "^7.0.5",
"i18next": "^23.14.0",
"intl-tel-input": "^24.3.6",
"intl-tel-input": "^24.4.0",
"js-md5": "^0.8.3",
"langchain": "^0.2.17",
"langchain": "^0.2.18",
"lodash": "^4.17.21",
"lru-cache": "^11.0.0",
"lucide-react": "^0.438.0",
"lru-cache": "^11.0.1",
"lucide-react": "^0.439.0",
"mark.js": "^8.11.1",
"microsoft-cognitiveservices-speech-sdk": "^1.40.0",
"mustache": "^4.2.0",
"next-themes": "^0.3.0",
"openai": "^4.57.0",
"openai": "^4.58.0",
"pitchfinder": "^2.3.2",
"postcss": "^8.4.43",
"postcss": "^8.4.45",
"proxy-agent": "^6.4.0",
"react": "^18.3.1",
"react-activity-calendar": "^2.5.1",
@@ -171,7 +171,7 @@
"react-dom": "^18.3.1",
"react-frame-component": "^5.2.7",
"react-hook-form": "^7.53.0",
"react-hotkeys-hook": "^4.5.0",
"react-hotkeys-hook": "^4.5.1",
"react-i18next": "^15.0.1",
"react-markdown": "^9.0.1",
"react-resizable-panels": "^2.1.2",

View File

@@ -14,8 +14,17 @@ export class Client {
accessToken?: string;
logger?: any;
locale?: "en" | "zh-CN";
onError?: (err: any) => void;
onSuccess?: (res: any) => void;
}) {
const { baseUrl, accessToken, logger, locale = "en" } = options;
const {
baseUrl,
accessToken,
logger,
locale = "en",
onError,
onSuccess,
} = options;
this.baseUrl = baseUrl;
this.logger = logger || console;
@@ -40,6 +49,10 @@ export class Client {
});
this.api.interceptors.response.use(
(response) => {
if (onSuccess) {
onSuccess(response);
}
this.logger.debug(
response.status,
response.config.method.toUpperCase(),
@@ -48,14 +61,22 @@ export class Client {
return camelcaseKeys(response.data, { deep: true });
},
(err) => {
if (onError) {
onError(err);
}
if (err.response) {
this.logger.error(
err.response.status,
err.response.config.method.toUpperCase(),
err.response.config.baseURL + err.response.config.url
err.response.config.baseURL + err.response.config.url,
err.response.data
);
this.logger.error(err.response.data);
return Promise.reject(new Error(err.response.data));
if (err.response.data) {
err.message = err.response.data;
}
return Promise.reject(err);
}
this.logger.error(err.message);

View File

@@ -401,10 +401,10 @@
"sttAiService": "STT AI service",
"local": "Local",
"localSpeechToTextDescription": "Use local whisper model to transcribe. It is free.",
"azureAi": "Azure AI",
"azureSpeechToTextDescription": "Use Azure AI Speech to transcribe. It is a paid service.",
"cloudflareAi": "Cloudflare AI",
"cloudflareSpeechToTextDescription": "Use Cloudflare AI Worker to transcribe. It is in beta and free for now.",
"enjoyAzure": "Azure AI (by Enjoy)",
"enjoyAzureSpeechToTextDescription": "Use Azure AI Speech to transcribe. It is a paid service from Enjoy.",
"enjoyCloudflare": "Cloudflare AI (by Enjoy)",
"enjoyCloudflareSpeechToTextDescription": "Use Cloudflare AI Worker to transcribe. It is in beta and free for now.",
"openaiSpeechToTextDescription": "Use openAI to transcribe using your own key.",
"uploadSpeechToTextDescription": "Upload transcript file or input transcript text to align.",
"checkingWhisper": "Checking whisper status",
@@ -781,5 +781,7 @@
"selectDir": "Select Folder",
"selectFile": "Select File",
"importMdictFile": "Import the original dictionary file",
"mdictFileTip": "Directly import .mdx .mdd format files (.mdx files are required, .mdd files are optional and can have multiple), but there may be problems with style and usability."
"mdictFileTip": "Directly import .mdx .mdd format files (.mdx files are required, .mdd files are optional and can have multiple), but there may be problems with style and usability.",
"authorizationExpired": "Your authorization has expired. Please login again.",
"selectUser": "Select user"
}

View File

@@ -401,11 +401,11 @@
"sttAiService": "语音转文本服务",
"local": "本地",
"localSpeechToTextDescription": "使用本地 whisper 模型进行语音转文本,不会产生费用",
"azureAi": "Azure AI",
"azureSpeechToTextDescription": "使用 Azure AI Speech 进行语音转文本,收费服务",
"cloudflareAi": "Cloudflare AI",
"cloudflareSpeechToTextDescription": "使用 Cloudflare AI 进行语音转文本,目前免费",
"openaiSpeechToTextDescription": "使用 OpenAI 进行语音转文本(需要 API 密钥)",
"enjoyAzure": "Azure (Enjoy 提供)",
"enjoyAzureSpeechToTextDescription": "使用 Azure AI Speech 进行语音转文本,Enjoy 提供的收费服务",
"enjoyCloudflare": "Cloudflare AI (Enjoy 提供)",
"enjoyCloudflareSpeechToTextDescription": "使用 Cloudflare AI Worker 进行语音转文本,Enjoy 提供的免费服务,有一定的使用限额",
"openaiSpeechToTextDescription": "使用 OpenAI 进行语音转文本(需要自备 API 密钥)",
"uploadSpeechToTextDescription": "上传字幕文件或者输入文本进行字幕对齐",
"checkingWhisper": "正在检查 Whisper",
"pleaseDownloadWhisperModelFirst": "请先下载 Whisper 模型",
@@ -781,5 +781,7 @@
"selectDir": "选择文件夹",
"selectFile": "选择文件",
"importMdictFile": "导入原词典文件",
"mdictFileTip": "直接导入 .mdx .mdd 格式的文件 (.mdx 文件是必须的,.mdd 文件是可选的且可以有多个),不过样式和可用性可能存在问题。"
"mdictFileTip": "直接导入 .mdx .mdd 格式的文件 (.mdx 文件是必须的,.mdd 文件是可选的且可以有多个),不过样式和可用性可能存在问题。",
"authorizationExpired": "您的登录授权已过期,请重新登录。",
"selectUser": "选择用户"
}

View File

@@ -2,7 +2,6 @@ import { app, BrowserWindow, protocol, net } from "electron";
import path from "path";
import fs from "fs-extra";
import settings from "@main/settings";
import "@main/i18n";
import log from "@main/logger";
import mainWindow from "@main/window";
import ElectronSquirrelStartup from "electron-squirrel-startup";

View File

@@ -188,6 +188,17 @@ class AudiosHandler {
ipcMain.handle("audios-crop", this.crop);
ipcMain.handle("audios-clean-up", this.cleanUp);
}
unregister() {
ipcMain.removeHandler("audios-find-all");
ipcMain.removeHandler("audios-find-one");
ipcMain.removeHandler("audios-create");
ipcMain.removeHandler("audios-update");
ipcMain.removeHandler("audios-destroy");
ipcMain.removeHandler("audios-upload");
ipcMain.removeHandler("audios-crop");
ipcMain.removeHandler("audios-clean-up");
}
}
export const audiosHandler = new AudiosHandler();

View File

@@ -82,6 +82,14 @@ class CacheObjectsHandler {
ipcMain.handle("cache-objects-clear", this.clear);
ipcMain.handle("cache-objects-write-file", this.writeFile);
}
unregister() {
ipcMain.removeHandler("cache-objects-get");
ipcMain.removeHandler("cache-objects-set");
ipcMain.removeHandler("cache-objects-delete");
ipcMain.removeHandler("cache-objects-clear");
ipcMain.removeHandler("cache-objects-write-file");
}
}
export const cacheObjectsHandler = new CacheObjectsHandler();

View File

@@ -81,6 +81,14 @@ class ChatAgentsHandler {
ipcMain.handle("chat-agents-update", this.update);
ipcMain.handle("chat-agents-destroy", this.destroy);
}
unregister() {
ipcMain.removeHandler("chat-agents-find-all");
ipcMain.removeHandler("chat-agents-find-one");
ipcMain.removeHandler("chat-agents-create");
ipcMain.removeHandler("chat-agents-update");
ipcMain.removeHandler("chat-agents-destroy");
}
}
export const chatAgentsHandler = new ChatAgentsHandler();

View File

@@ -13,12 +13,15 @@ class ChatMembersHandler {
private async findAll(
_event: IpcMainEvent,
options: FindOptions<Attributes<ChatMember>> & { query?: string }
) {
}
) {}
register() {
ipcMain.handle("chat-members-find-all", this.findAll);
}
unregister() {
ipcMain.removeHandler("chat-members-find-all");
}
}
export const chatMembersHandler = new ChatMembersHandler();
export const chatMembersHandler = new ChatMembersHandler();

View File

@@ -141,6 +141,14 @@ class ChatMessagesHandler {
ipcMain.handle("chat-messages-update", this.update);
ipcMain.handle("chat-messages-destroy", this.destroy);
}
unregister() {
ipcMain.removeHandler("chat-messages-find-all");
ipcMain.removeHandler("chat-messages-find-one");
ipcMain.removeHandler("chat-messages-create");
ipcMain.removeHandler("chat-messages-update");
ipcMain.removeHandler("chat-messages-destroy");
}
}
export const chatMessagesHandler = new ChatMessagesHandler();

View File

@@ -1,10 +1,10 @@
import { ipcMain, IpcMainEvent } from "electron";
import { Chat, ChatAgent, ChatMember } from "@main/db/models";
import { Chat, ChatAgent, ChatMember, UserSetting } from "@main/db/models";
import { FindOptions, WhereOptions, Attributes, Op } from "sequelize";
import log from "@main/logger";
import { t } from "i18next";
import db from "@main/db";
import settings from "@/main/settings";
import { UserSettingKeyEnum } from "@/types/enums";
const logger = log.scope("db/handlers/chats-handler");
@@ -73,7 +73,9 @@ class ChatsHandler {
const transaction = await db.connection.transaction();
if (!chatData.config?.sttEngine) {
chatData.config.sttEngine = settings.whisperConfig().service;
chatData.config.sttEngine = (await UserSetting.get(
UserSettingKeyEnum.STT_ENGINE
)) as string;
}
const chat = await Chat.create(chatData, {
transaction,
@@ -195,6 +197,14 @@ class ChatsHandler {
ipcMain.handle("chats-update", this.update);
ipcMain.handle("chats-destroy", this.destroy);
}
unregister() {
ipcMain.removeHandler("chats-find-all");
ipcMain.removeHandler("chats-find-one");
ipcMain.removeHandler("chats-create");
ipcMain.removeHandler("chats-update");
ipcMain.removeHandler("chats-destroy");
}
}
export const chatsHandler = new ChatsHandler();

View File

@@ -120,6 +120,14 @@ class ConversationsHandler {
ipcMain.handle("conversations-update", this.update);
ipcMain.handle("conversations-destroy", this.destroy);
}
unregister() {
ipcMain.removeHandler("conversations-find-all");
ipcMain.removeHandler("conversations-find-one");
ipcMain.removeHandler("conversations-create");
ipcMain.removeHandler("conversations-update");
ipcMain.removeHandler("conversations-destroy");
}
}
export const conversationsHandler = new ConversationsHandler();

View File

@@ -12,4 +12,5 @@ export * from "./recordings-handler";
export * from "./speeches-handler";
export * from "./segments-handler";
export * from "./transcriptions-handler";
export * from "./user-settings-handler";
export * from "./videos-handler";

View File

@@ -177,6 +177,16 @@ class MessagesHandler {
ipcMain.handle("messages-destroy", this.destroy);
ipcMain.handle("messages-create-speech", this.createSpeech);
}
unregister() {
ipcMain.removeHandler("messages-find-all");
ipcMain.removeHandler("messages-find-one");
ipcMain.removeHandler("messages-create");
ipcMain.removeHandler("messages-create-in-batch");
ipcMain.removeHandler("messages-update");
ipcMain.removeHandler("messages-destroy");
ipcMain.removeHandler("messages-create-speech");
}
}
export const messagesHandler = new MessagesHandler();

View File

@@ -165,6 +165,17 @@ class NotesHandler {
ipcMain.handle("notes-create", this.create);
ipcMain.handle("notes-sync", this.sync);
}
unregister() {
ipcMain.removeHandler("notes-group-by-target");
ipcMain.removeHandler("notes-group-by-segment");
ipcMain.removeHandler("notes-find-all");
ipcMain.removeHandler("notes-find");
ipcMain.removeHandler("notes-update");
ipcMain.removeHandler("notes-delete");
ipcMain.removeHandler("notes-create");
ipcMain.removeHandler("notes-sync");
}
}
export const notesHandler = new NotesHandler();

View File

@@ -102,6 +102,14 @@ class PronunciationAssessmentsHandler {
ipcMain.handle("pronunciation-assessments-update", this.update);
ipcMain.handle("pronunciation-assessments-destroy", this.destroy);
}
unregister() {
ipcMain.removeHandler("pronunciation-assessments-find-all");
ipcMain.removeHandler("pronunciation-assessments-find-one");
ipcMain.removeHandler("pronunciation-assessments-create");
ipcMain.removeHandler("pronunciation-assessments-update");
ipcMain.removeHandler("pronunciation-assessments-destroy");
}
}
export const pronunciationAssessmentsHandler =

View File

@@ -342,6 +342,20 @@ class RecordingsHandler {
ipcMain.handle("recordings-group-by-target", this.groupByTarget);
ipcMain.handle("recordings-group-by-segment", this.groupBySegment);
}
unregister() {
ipcMain.removeHandler("recordings-find-all");
ipcMain.removeHandler("recordings-find-one");
ipcMain.removeHandler("recordings-sync");
ipcMain.removeHandler("recordings-sync-all");
ipcMain.removeHandler("recordings-create");
ipcMain.removeHandler("recordings-destroy");
ipcMain.removeHandler("recordings-upload");
ipcMain.removeHandler("recordings-stats");
ipcMain.removeHandler("recordings-group-by-date");
ipcMain.removeHandler("recordings-group-by-target");
ipcMain.removeHandler("recordings-group-by-segment");
}
}
export const recordingsHandler = new RecordingsHandler();

View File

@@ -57,6 +57,13 @@ class SegmentsHandler {
ipcMain.handle("segments-find-all", this.findAll);
ipcMain.handle("segments-sync", this.sync);
}
unregister() {
ipcMain.removeHandler("segments-create");
ipcMain.removeHandler("segments-find");
ipcMain.removeHandler("segments-find-all");
ipcMain.removeHandler("segments-sync");
}
}
export const segmentsHandler = new SegmentsHandler();

View File

@@ -59,6 +59,11 @@ class SpeechesHandler {
ipcMain.handle("speeches-find-one", this.findOne);
ipcMain.handle("speeches-create", this.create);
}
unregister() {
ipcMain.removeHandler("speeches-find-one");
ipcMain.removeHandler("speeches-create");
}
}
export const speechesHandler = new SpeechesHandler();

View File

@@ -60,6 +60,11 @@ class TranscriptionsHandler {
ipcMain.handle("transcriptions-find-or-create", this.findOrCreate);
ipcMain.handle("transcriptions-update", this.update);
}
unregister() {
ipcMain.removeHandler("transcriptions-find-or-create");
ipcMain.removeHandler("transcriptions-update");
}
}
export const transcriptionsHandler = new TranscriptionsHandler();

View File

@@ -0,0 +1,79 @@
import { ipcMain, IpcMainEvent } from "electron";
import { UserSetting } from "@main/db/models";
import db from "@main/db";
import { UserSettingKeyEnum } from "@/types/enums";
class UserSettingsHandler {
private async get(event: IpcMainEvent, key: UserSettingKeyEnum) {
return UserSetting.get(key)
.then((value) => {
return value;
})
.catch((err) => {
event.sender.send("on-notification", {
type: "error",
message: err.message,
});
});
}
private async set(
event: IpcMainEvent,
key: UserSettingKeyEnum,
value: string | object
) {
return UserSetting.set(key, value)
.then(() => {
return;
})
.catch((err) => {
event.sender.send("on-notification", {
type: "error",
message: err.message,
});
});
}
private async delete(event: IpcMainEvent, key: UserSettingKeyEnum) {
return UserSetting.destroy({ where: { key } })
.then(() => {
return;
})
.catch((err) => {
event.sender.send("on-notification", {
type: "error",
message: err.message,
});
});
}
private async clear(event: IpcMainEvent) {
return UserSetting.destroy({ where: {} })
.then(() => {
db.connection.query("VACUUM");
return;
})
.catch((err) => {
event.sender.send("on-notification", {
type: "error",
message: err.message,
});
});
}
register() {
ipcMain.handle("user-settings-get", this.get);
ipcMain.handle("user-settings-set", this.set);
ipcMain.handle("user-settings-delete", this.delete);
ipcMain.handle("user-settings-clear", this.clear);
}
unregister() {
ipcMain.removeHandler("user-settings-get");
ipcMain.removeHandler("user-settings-set");
ipcMain.removeHandler("user-settings-delete");
ipcMain.removeHandler("user-settings-clear");
}
}
export const userSettingsHandler = new UserSettingsHandler();

View File

@@ -178,6 +178,17 @@ class VideosHandler {
ipcMain.handle("videos-crop", this.crop);
ipcMain.handle("videos-clean-up", this.cleanUp);
}
unregister() {
ipcMain.removeHandler("videos-find-all");
ipcMain.removeHandler("videos-find-one");
ipcMain.removeHandler("videos-create");
ipcMain.removeHandler("videos-update");
ipcMain.removeHandler("videos-destroy");
ipcMain.removeHandler("videos-upload");
ipcMain.removeHandler("videos-crop");
ipcMain.removeHandler("videos-clean-up");
}
}
export const videosHandler = new VideosHandler();

View File

@@ -6,6 +6,10 @@ import {
Audio,
Recording,
CacheObject,
Chat,
ChatAgent,
ChatMember,
ChatMessage,
Conversation,
Message,
Note,
@@ -14,14 +18,15 @@ import {
Speech,
Transcription,
Video,
Chat,
ChatAgent,
ChatMember,
ChatMessage,
UserSetting,
} from "./models";
import {
audiosHandler,
cacheObjectsHandler,
chatAgentsHandler,
chatMembersHandler,
chatMessagesHandler,
chatsHandler,
conversationsHandler,
messagesHandler,
notesHandler,
@@ -31,14 +36,13 @@ import {
speechesHandler,
transcriptionsHandler,
videosHandler,
chatAgentsHandler,
chatMembersHandler,
chatMessagesHandler,
chatsHandler,
userSettingsHandler,
} from "./handlers";
import os from "os";
import path from "path";
import url from "url";
import { i18n } from "@main/i18n";
import { UserSettingKeyEnum } from "@/types/enums";
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -46,6 +50,7 @@ const __dirname = path.dirname(__filename);
const db = {
connection: null as Sequelize | null,
connect: async () => {},
disconnect: async () => {},
registerIpcHandlers: () => {},
};
@@ -53,12 +58,21 @@ db.connect = async () => {
if (db.connection) {
return;
}
const dbPath = settings.dbPath();
if (!dbPath) {
throw new Error("Db path is not ready");
}
const sequelize = new Sequelize({
dialect: "sqlite",
storage: settings.dbPath(),
storage: dbPath,
models: [
Audio,
CacheObject,
Chat,
ChatAgent,
ChatMember,
ChatMessage,
Conversation,
Message,
Note,
@@ -67,11 +81,8 @@ db.connect = async () => {
Segment,
Speech,
Transcription,
UserSetting,
Video,
Chat,
ChatAgent,
ChatMember,
ChatMessage,
],
});
@@ -143,12 +154,25 @@ db.connect = async () => {
});
});
// migrate settings
await UserSetting.migrateFromSettings();
// initialize i18n
const language = (await UserSetting.get(
UserSettingKeyEnum.LANGUAGE
)) as string;
i18n(language);
// vacuum the database
await sequelize.query("VACUUM");
// register handlers
audiosHandler.register();
cacheObjectsHandler.register();
chatAgentsHandler.register();
chatMembersHandler.register();
chatMessagesHandler.register();
chatsHandler.register();
conversationsHandler.register();
messagesHandler.register();
notesHandler.register();
@@ -157,28 +181,53 @@ db.connect = async () => {
segmentsHandler.register();
speechesHandler.register();
transcriptionsHandler.register();
userSettingsHandler.register();
videosHandler.register();
chatAgentsHandler.register();
chatMembersHandler.register();
chatMessagesHandler.register();
chatsHandler.register();
};
db.disconnect = async () => {
// unregister handlers
audiosHandler.unregister();
cacheObjectsHandler.unregister();
chatAgentsHandler.unregister();
chatMembersHandler.unregister();
chatMessagesHandler.unregister();
chatsHandler.unregister();
conversationsHandler.unregister();
messagesHandler.unregister();
notesHandler.unregister();
pronunciationAssessmentsHandler.unregister();
recordingsHandler.unregister();
segmentsHandler.unregister();
speechesHandler.unregister();
transcriptionsHandler.unregister();
userSettingsHandler.unregister();
videosHandler.unregister();
await db.connection?.close();
db.connection = null;
};
db.registerIpcHandlers = () => {
ipcMain.handle("db-init", async () => {
return db
.connect()
.then(() => {
return {
state: "connected",
};
})
.catch((err) => {
return {
state: "error",
error: err.message,
};
});
ipcMain.handle("db-connect", async () => {
try {
await db.connect();
return {
state: "connected",
path: settings.dbPath(),
error: null,
};
} catch (err) {
return {
state: "error",
error: err.message,
path: settings.dbPath(),
};
}
});
ipcMain.handle("db-disconnect", async () => {
db.disconnect();
});
};

View File

@@ -0,0 +1,45 @@
import { DataTypes } from "sequelize";
async function up({ context: queryInterface }) {
queryInterface.createTable(
"user_settings",
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false,
},
key: {
type: DataTypes.STRING,
allowNull: false,
},
value: {
type: DataTypes.TEXT,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
indexes: [
{
unique: true,
fields: ["key"],
},
],
}
);
}
async function down({ context: queryInterface }) {
queryInterface.dropTable("user_settings");
}
export { up, down };

View File

@@ -11,5 +11,6 @@ export * from "./pronunciation-assessment";
export * from "./recording";
export * from "./segment";
export * from "./speech";
export * from "./user-setting";
export * from "./transcription";
export * from "./video";

View File

@@ -0,0 +1,176 @@
import {
Table,
Column,
Default,
IsUUID,
Model,
DataType,
AllowNull,
} from "sequelize-typescript";
import log from "@main/logger";
import settings from "@main/settings";
import * as i18n from "i18next";
import { SttEngineOptionEnum, UserSettingKeyEnum } from "@/types/enums";
const logger = log.scope("db/userSetting");
@Table({
modelName: "UserSetting",
tableName: "user_settings",
underscored: true,
timestamps: true,
})
export class UserSetting extends Model<UserSetting> {
@IsUUID(4)
@Default(DataType.UUIDV4)
@Column({ primaryKey: true, type: DataType.UUID })
id: string;
@Column(DataType.STRING)
key: string;
@AllowNull(false)
@Column(DataType.TEXT)
value: string;
static async get(
key: UserSettingKeyEnum
): Promise<string | Object | number | null> {
const setting = await UserSetting.findOne({ where: { key } });
if (!setting) return null;
try {
return JSON.parse(setting.value);
} catch {
return setting.value;
}
}
static async set(
key: UserSettingKeyEnum,
value: object | string
): Promise<void> {
const setting = await UserSetting.findOne({ where: { key } });
if (typeof value === "object") {
value = JSON.stringify(value);
}
if (setting) {
await setting.update({ value });
} else {
await UserSetting.create({ key, value });
}
// update i18n
if (key === UserSettingKeyEnum.LANGUAGE) {
i18n.changeLanguage(value);
}
}
static async migrateFromSettings(): Promise<void> {
// hotkeys
const hotkeys = await UserSetting.get(UserSettingKeyEnum.HOTKEYS);
const prevHotkeys = await settings.get("defaultHotkeys");
if (prevHotkeys && !hotkeys) {
UserSetting.set(UserSettingKeyEnum.HOTKEYS, prevHotkeys as object);
}
// GPT Engine
const gptEngine = await UserSetting.get(UserSettingKeyEnum.GPT_ENGINE);
const prevGptEngine = await settings.get("engine.gpt");
if (prevGptEngine && !gptEngine) {
UserSetting.set(UserSettingKeyEnum.GPT_ENGINE, prevGptEngine as object);
}
// OpenAI API Key
const openai = await UserSetting.get(UserSettingKeyEnum.OPENAI);
const prevOpenai = await settings.get("openai");
if (prevOpenai && !openai) {
UserSetting.set(UserSettingKeyEnum.OPENAI, prevOpenai as object);
}
// Language
const language = await UserSetting.get(UserSettingKeyEnum.LANGUAGE);
const prevLanguage = await settings.get("language");
if (prevLanguage && !language) {
UserSetting.set(UserSettingKeyEnum.LANGUAGE, prevLanguage as string);
}
// Native Language
const nativeLanguage = await UserSetting.get(
UserSettingKeyEnum.NATIVE_LANGUAGE
);
const prevNativeLanguage = await settings.get("nativeLanguage");
if (prevNativeLanguage && !nativeLanguage) {
UserSetting.set(
UserSettingKeyEnum.NATIVE_LANGUAGE,
prevNativeLanguage as string
);
}
// Learning Language
const learningLanguage = await UserSetting.get(
UserSettingKeyEnum.LEARNING_LANGUAGE
);
const prevLearningLanguage = await settings.get("learningLanguage");
if (prevLearningLanguage && !learningLanguage) {
UserSetting.set(
UserSettingKeyEnum.LEARNING_LANGUAGE,
prevLearningLanguage as string
);
}
// STT Engine
const sttEngine = await UserSetting.get(UserSettingKeyEnum.STT_ENGINE);
const prevSttEngine = await settings.get("whisper.service");
if (prevSttEngine && !sttEngine) {
switch (prevSttEngine) {
case "azure":
UserSetting.set(
UserSettingKeyEnum.STT_ENGINE,
SttEngineOptionEnum.ENJOY_AZURE
);
break;
case "cloudflare":
UserSetting.set(
UserSettingKeyEnum.STT_ENGINE,
SttEngineOptionEnum.ENJOY_CLOUDFLARE
);
break;
case "openai":
UserSetting.set(
UserSettingKeyEnum.STT_ENGINE,
SttEngineOptionEnum.OPENAI
);
break;
default:
break;
}
}
// Whisper
const whisper = await UserSetting.get(UserSettingKeyEnum.WHISPER);
const prevWhisper = await settings.get("whisper.model");
if (prevWhisper && !whisper) {
UserSetting.set(UserSettingKeyEnum.WHISPER, prevWhisper as string);
}
// Profile
const profile = await UserSetting.get(UserSettingKeyEnum.PROFILE);
const prevProfile = (await settings.get("user")) as UserType;
if (prevProfile && !profile) {
UserSetting.set(UserSettingKeyEnum.PROFILE, prevProfile as UserType);
}
// Recorder Config
const recorderConfig = await UserSetting.get(UserSettingKeyEnum.RECORDER);
const prevRecorderConfig = await settings.get("recorderConfig");
if (prevRecorderConfig && !recorderConfig) {
UserSetting.set(
UserSettingKeyEnum.RECORDER,
prevRecorderConfig as string
);
}
}
}

View File

@@ -1,7 +1,6 @@
import * as i18n from "i18next";
import * as i18next from "i18next";
import en from "@/i18n/en.json";
import zh_CN from "@/i18n/zh-CN.json";
import settings from "@main/settings";
const resources = {
en: {
@@ -12,14 +11,14 @@ const resources = {
},
};
i18n.init({
resources,
lng: settings.language(),
supportedLngs: ["en", "zh-CN"],
fallbackLng: "en",
interpolation: {
escapeValue: false, // react already safes from xss
},
});
export default i18n;
export const i18n = (language: string) => {
i18next.init({
resources,
lng: language,
supportedLngs: ["en", "zh-CN"],
fallbackLng: "en",
interpolation: {
escapeValue: false, // react already safes from xss
},
});
};

View File

@@ -3,7 +3,7 @@ import { LIBRARY_PATH_SUFFIX, DATABASE_NAME, WEB_API_URL } from "@/constants";
import { ipcMain, app } from "electron";
import path from "path";
import fs from "fs-extra";
import * as i18n from "i18next";
import { AppSettingsKeyEnum } from "@/types/enums";
if (process.env.SETTINGS_PATH) {
settings.configure({
@@ -12,35 +12,23 @@ if (process.env.SETTINGS_PATH) {
});
}
const language = () => {
const _language = settings.getSync("language");
if (!_language || typeof _language !== "string") {
settings.setSync("language", "en");
}
return settings.getSync("language") as string;
};
const switchLanguage = (language: string) => {
settings.setSync("language", language);
i18n.changeLanguage(language);
};
const libraryPath = () => {
const _library = settings.getSync("library");
if (!_library || typeof _library !== "string") {
settings.setSync(
"library",
AppSettingsKeyEnum.LIBRARY,
process.env.LIBRARY_PATH ||
path.join(app.getPath("documents"), LIBRARY_PATH_SUFFIX)
);
} else if (path.parse(_library).base !== LIBRARY_PATH_SUFFIX) {
settings.setSync("library", path.join(_library, LIBRARY_PATH_SUFFIX));
settings.setSync(
AppSettingsKeyEnum.LIBRARY,
path.join(_library, LIBRARY_PATH_SUFFIX)
);
}
const library = settings.getSync("library") as string;
const library = settings.getSync(AppSettingsKeyEnum.LIBRARY) as string;
fs.ensureDirSync(library);
return library;
@@ -54,165 +42,84 @@ const cachePath = () => {
};
const dbPath = () => {
if (!userDataPath()) return null;
const dbName = app.isPackaged
? `${DATABASE_NAME}.sqlite`
: `${DATABASE_NAME}_dev.sqlite`;
return path.join(userDataPath(), dbName);
};
const whisperConfig = (): WhisperConfigType => {
const model = settings.getSync("whisper.model") as string;
let service = settings.getSync(
"whisper.service"
) as WhisperConfigType["service"];
if (!service) {
settings.setSync("whisper.service", "azure");
service = "azure";
}
return {
service,
availableModels: settings.getSync(
"whisper.availableModels"
) as WhisperConfigType["availableModels"],
modelsPath: settings.getSync("whisper.modelsPath") as string,
model,
};
};
const userDataPath = () => {
const userData = path.join(
libraryPath(),
settings.getSync("user.id").toString()
);
const userId = settings.getSync("user.id");
if (!userId) return null;
const userData = path.join(libraryPath(), userId.toString());
fs.ensureDirSync(userData);
return userData;
};
const apiUrl = () => {
const url: string = settings.getSync("apiUrl") as string;
const url: string = settings.getSync(AppSettingsKeyEnum.API_URL) as string;
return process.env.WEB_API_URL || url || WEB_API_URL;
};
// scan library directory and get all user data directories
// the name of user data directory is the user id, and they are all numbers and 8 digits
const sessions = () => {
const library = libraryPath();
const sessions = fs.readdirSync(library).filter((dir) => {
return dir.match(/^\d{8}$/);
});
return sessions.map((id) => ({ id: parseInt(id), name: id }));
};
export default {
registerIpcHandlers: () => {
ipcMain.handle("settings-get", (_event, key) => {
return settings.getSync(key);
});
ipcMain.handle("settings-set", (_event, key, value) => {
settings.setSync(key, value);
});
ipcMain.handle("settings-get-library", (_event) => {
ipcMain.handle("app-settings-get-library", (_event) => {
libraryPath();
return settings.getSync("library");
return settings.getSync(AppSettingsKeyEnum.LIBRARY);
});
ipcMain.handle("settings-set-library", (_event, library) => {
ipcMain.handle("app-settings-set-library", (_event, library) => {
if (path.parse(library).base === LIBRARY_PATH_SUFFIX) {
settings.setSync("library", library);
settings.setSync(AppSettingsKeyEnum.LIBRARY, library);
} else {
const dir = path.join(library, LIBRARY_PATH_SUFFIX);
fs.ensureDirSync(dir);
settings.setSync("library", dir);
settings.setSync(AppSettingsKeyEnum.LIBRARY, dir);
}
});
ipcMain.handle("settings-get-user", (_event) => {
return settings.getSync("user");
ipcMain.handle("app-settings-get-user", (_event) => {
return settings.getSync(AppSettingsKeyEnum.USER);
});
ipcMain.handle("settings-set-user", (_event, user) => {
settings.setSync("user", user);
ipcMain.handle("app-settings-set-user", (_event, user) => {
settings.setSync(AppSettingsKeyEnum.USER, user);
});
ipcMain.handle("settings-get-whisper-model", (_event) => {
return settings.getSync("whisper.model");
});
ipcMain.handle("settings-set-whisper-model", (_event, model) => {
settings.setSync("whisper.model", model);
});
ipcMain.handle("settings-get-user-data-path", (_event) => {
ipcMain.handle("app-settings-get-user-data-path", (_event) => {
return userDataPath();
});
ipcMain.handle("settings-get-llm", (_event, provider) => {
return settings.getSync(provider);
ipcMain.handle("app-settings-get-api-url", (_event) => {
return settings.getSync(AppSettingsKeyEnum.API_URL);
});
ipcMain.handle("settings-set-llm", (_event, provider, config) => {
return settings.setSync(provider, config);
ipcMain.handle("app-settings-set-api-url", (_event, url) => {
settings.setSync(AppSettingsKeyEnum.API_URL, url);
});
ipcMain.handle("settings-get-language", (_event) => {
return language();
});
ipcMain.handle("settings-switch-language", (_event, language) => {
switchLanguage(language);
});
ipcMain.handle("settings-get-default-engine", (_event) => {
return settings.getSync("defaultEngine");
});
ipcMain.handle("settings-set-default-engine", (_event, engine) => {
return settings.setSync("defaultEngine", engine);
});
ipcMain.handle("settings-get-gpt-engine", (_event) => {
return settings.getSync("engine.gpt");
});
ipcMain.handle("settings-set-gpt-engine", (_event, engine) => {
return settings.setSync("engine.gpt", engine);
});
ipcMain.handle("settings-get-default-hotkeys", (_event) => {
return settings.getSync("defaultHotkeys");
});
ipcMain.handle("settings-set-default-hotkeys", (_event, records) => {
return settings.setSync("defaultHotkeys", records);
});
ipcMain.handle("settings-get-dict", (_event) => {
return settings.getSync("dicts");
});
ipcMain.handle("settings-set-dicts", (_event, dict) => {
return settings.setSync("dicts", dict);
});
ipcMain.handle("settings-get-api-url", (_event) => {
return settings.getSync("apiUrl");
});
ipcMain.handle("settings-set-api-url", (_event, url) => {
return settings.setSync("apiUrl", url);
});
ipcMain.handle("settings-get-vocabulary-config", (_event) => {
return settings.getSync("vocabularyConfig");
});
ipcMain.handle("settings-set-vocabulary-config", (_event, records) => {
return settings.setSync("vocabularyConfig", records);
ipcMain.handle("app-settings-get-sessions", (_event) => {
return sessions();
});
},
cachePath,
libraryPath,
userDataPath,
dbPath,
whisperConfig,
language,
switchLanguage,
apiUrl,
...settings,
};

View File

@@ -8,6 +8,9 @@ import log from "@main/logger";
import url from "url";
import { enjoyUrlToPath } from "./utils";
import { t } from "i18next";
import { UserSetting } from "@main/db/models";
import db from "@main/db";
import { UserSettingKeyEnum } from "@/types/enums";
const __filename = url.fileURLToPath(import.meta.url);
/*
@@ -40,7 +43,7 @@ class Whipser {
this.initialize();
}
initialize() {
async initialize() {
const models = [];
const bundledModels = fs.readdirSync(this.bundledModelsDir);
@@ -66,9 +69,23 @@ class Whipser {
savePath: path.join(dir, file),
});
}
settings.setSync("whisper.availableModels", models);
settings.setSync("whisper.modelsPath", dir);
this.config = settings.whisperConfig();
if (db.connection) {
const whisperConfig = (await UserSetting.get(
UserSettingKeyEnum.WHISPER
)) as string;
this.config = {
model: whisperConfig || models[0].name,
availableModels: models,
modelsPath: dir,
};
} else {
this.config = {
model: models[0].name,
availableModels: models,
modelsPath: dir,
};
}
}
currentModel() {
@@ -82,9 +99,10 @@ class Whipser {
}
if (!model) {
model = this.config.availableModels[0];
this.config = Object.assign({}, this.config, { model: model.name });
UserSetting.set(UserSettingKeyEnum.WHISPER, model.name);
}
settings.setSync("whisper.model", model.name);
return model;
}
@@ -258,13 +276,13 @@ class Whipser {
registerIpcHandlers() {
ipcMain.handle("whisper-config", async () => {
await this.initialize();
return this.config;
});
ipcMain.handle("whisper-set-model", async (_event, model) => {
const originalModel = settings.getSync("whisper.model");
settings.setSync("whisper.model", model);
this.config = settings.whisperConfig();
const originalModel = this.config.model;
this.config.model = model;
return this.check()
.then(({ success, log }) => {
@@ -275,26 +293,14 @@ class Whipser {
}
})
.catch((err) => {
settings.setSync("whisper.model", originalModel);
this.config.model = originalModel;
throw err;
})
.finally(() => {
UserSetting.set(UserSettingKeyEnum.WHISPER, this.config.model);
});
});
ipcMain.handle("whisper-set-service", async (_event, service) => {
if (service === "local") {
await this.check();
settings.setSync("whisper.service", service);
this.config.service = service;
return this.config;
} else if (["cloudflare", "azure", "openai"].includes(service)) {
settings.setSync("whisper.service", service);
this.config.service = service;
return this.config;
} else {
throw new Error("Unknown service");
}
});
ipcMain.handle("whisper-check", async (_event) => {
return await this.check();
});

View File

@@ -14,7 +14,6 @@ import settings from "@main/settings";
import downloader from "@main/downloader";
import whisper from "@main/whisper";
import fs from "fs-extra";
import "@main/i18n";
import log from "@main/logger";
import { REPO_URL, WS_URL } from "@/constants";
import { AudibleProvider, TedProvider, YoutubeProvider } from "@main/providers";
@@ -42,7 +41,7 @@ const main = {
init: () => {},
};
main.init = () => {
main.init = async () => {
if (main.win) {
main.win.show();
return;

View File

@@ -182,75 +182,44 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
showErrorBox: (title: string, content: string) =>
ipcRenderer.invoke("dialog-show-error-box", title, content),
},
settings: {
appSettings: {
get: (key: string) => {
return ipcRenderer.invoke("settings-get", key);
return ipcRenderer.invoke("app-settings-get", key);
},
set: (key: string, value: any) => {
return ipcRenderer.invoke("settings-set", key, value);
return ipcRenderer.invoke("app-settings-set", key, value);
},
getLibrary: () => {
return ipcRenderer.invoke("settings-get-library");
return ipcRenderer.invoke("app-settings-get-library");
},
setLibrary: (library: string) => {
return ipcRenderer.invoke("settings-set-library", library);
return ipcRenderer.invoke("app-settings-set-library", library);
},
getSessions: () => {
return ipcRenderer.invoke("app-settings-get-sessions");
},
getUser: () => {
return ipcRenderer.invoke("settings-get-user");
return ipcRenderer.invoke("app-settings-get-user");
},
setUser: (user: UserType) => {
return ipcRenderer.invoke("settings-set-user", user);
return ipcRenderer.invoke("app-settings-set-user", user);
},
getUserDataPath: () => {
return ipcRenderer.invoke("settings-get-user-data-path");
},
getDefaultEngine: () => {
return ipcRenderer.invoke("settings-get-default-engine");
},
setDefaultEngine: (engine: "enjoyai" | "openai") => {
return ipcRenderer.invoke("settings-set-default-engine", engine);
},
getGptEngine: () => {
return ipcRenderer.invoke("settings-get-gpt-engine");
},
setGptEngine: (engine: GptEngineSettingType) => {
return ipcRenderer.invoke("settings-set-gpt-engine", engine);
},
getLlm: (provider: string) => {
return ipcRenderer.invoke("settings-get-llm", provider);
},
setLlm: (provider: string, config: LlmProviderType) => {
return ipcRenderer.invoke("settings-set-llm", provider, config);
},
getLanguage: (language: string) => {
return ipcRenderer.invoke("settings-get-language", language);
},
switchLanguage: (language: string) => {
return ipcRenderer.invoke("settings-switch-language", language);
},
getDefaultHotkeys: () => {
return ipcRenderer.invoke("settings-get-default-hotkeys");
},
setDefaultHotkeys: (records: Record<string, string>) => {
return ipcRenderer.invoke("settings-set-default-hotkeys", records);
},
getDictSettings: () => {
return ipcRenderer.invoke("settings-get-dict");
},
setDictSettings: (dict: DictSettingType) => {
return ipcRenderer.invoke("settings-set-dicts", dict);
return ipcRenderer.invoke("app-settings-get-user-data-path");
},
getApiUrl: () => {
return ipcRenderer.invoke("settings-get-api-url");
return ipcRenderer.invoke("app-settings-get-api-url");
},
setApiUrl: (url: string) => {
return ipcRenderer.invoke("settings-set-api-url", url);
return ipcRenderer.invoke("app-settings-set-api-url", url);
},
getVocabularyConfig: () => {
return ipcRenderer.invoke("settings-get-vocabulary-config");
},
userSettings: {
get: (key: string) => {
return ipcRenderer.invoke("user-settings-get", key);
},
setVocabularyConfig: (records: Record<string, string>) => {
return ipcRenderer.invoke("settings-set-vocabulary-config", records);
set: (key: string, value: any) => {
return ipcRenderer.invoke("user-settings-set", key, value);
},
},
path: {
@@ -259,7 +228,8 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
},
},
db: {
init: () => ipcRenderer.invoke("db-init"),
connect: () => ipcRenderer.invoke("db-connect"),
disconnect: () => ipcRenderer.invoke("db-disconnect"),
onTransaction: (
callback: (
event: IpcRendererEvent,
@@ -516,9 +486,6 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
setModel: (model: string) => {
return ipcRenderer.invoke("whisper-set-model", model);
},
setService: (service: string) => {
return ipcRenderer.invoke("whisper-set-service", service);
},
check: () => {
return ipcRenderer.invoke("whisper-check");
},

View File

@@ -35,21 +35,21 @@ function App() {
return (
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<AppSettingsProvider>
<HotKeysSettingsProvider>
<AISettingsProvider>
<DictProvider>
<DbProvider>
<DbProvider>
<AppSettingsProvider>
<HotKeysSettingsProvider>
<AISettingsProvider>
<DictProvider>
<RouterProvider router={router} />
<Toaster richColors closeButton position="top-center" />
<Tooltip id="global-tooltip" />
<TranslateWidget />
<LookupWidget />
</DbProvider>
</DictProvider>
</AISettingsProvider>
</HotKeysSettingsProvider>
</AppSettingsProvider>
</DictProvider>
</AISettingsProvider>
</HotKeysSettingsProvider>
</AppSettingsProvider>
</DbProvider>
</ThemeProvider>
);
}

View File

@@ -46,4 +46,8 @@ export class NoticiationsChannel {
}
);
}
unsubscribe() {
this.consumer.disconnect();
}
}

View File

@@ -192,9 +192,9 @@ export const ChatForm = (props: {
</SelectTrigger>
<SelectContent>
<SelectItem value="local">{t("local")}</SelectItem>
<SelectItem value="azure">{t("azureAi")}</SelectItem>
<SelectItem value="azure">{t("enjoyAzure")}</SelectItem>
<SelectItem value="cloudflare">
{t("cloudflareAi")}
{t("enjoyCloudflare")}
</SelectItem>
<SelectItem value="openai">OpenAI</SelectItem>
</SelectContent>
@@ -203,9 +203,9 @@ export const ChatForm = (props: {
{form.watch("config.sttEngine") === "local" &&
t("localSpeechToTextDescription")}
{form.watch("config.sttEngine") === "azure" &&
t("azureSpeechToTextDescription")}
t("enjoyAzureSpeechToTextDescription")}
{form.watch("config.sttEngine") === "cloudflare" &&
t("cloudflareSpeechToTextDescription")}
t("enjoyCloudflareSpeechToTextDescription")}
{form.watch("config.sttEngine") === "openai" &&
t("openaiSpeechToTextDescription")}
</FormDescription>

View File

@@ -14,14 +14,11 @@ import {
TabsList,
TabsTrigger,
} from "@renderer/components/ui";
import {
CheckCircleIcon,
CircleAlertIcon,
LoaderIcon,
} from "lucide-react";
import { CheckCircleIcon, CircleAlertIcon, LoaderIcon } from "lucide-react";
import { t } from "i18next";
import { useNavigate } from "react-router-dom";
import { TranscriptionCreateForm, TranscriptionsList } from "../transcriptions";
import { SttEngineOptionEnum } from "@/types/enums";
export const MediaLoadingModal = () => {
const navigate = useNavigate();
@@ -68,7 +65,7 @@ export const MediaLoadingModal = () => {
generateTranscription({
originalText: data.text,
language: data.language,
service: data.service as WhisperConfigType["service"],
service: data.service as SttEngineOptionEnum | "upload",
isolate: data.isolate,
});
}}

View File

@@ -6,6 +6,8 @@ import {
Sheet,
SheetTrigger,
SheetContent,
SheetHeader,
SheetTitle,
} from "@renderer/components/ui";
import { useContext, useEffect, useRef, useState } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
@@ -38,6 +40,9 @@ export const BanduLoginButton = () => {
className="h-screen"
aria-describedby={undefined}
>
<SheetHeader>
<SheetTitle className="sr-only"></SheetTitle>
</SheetHeader>
<div className="w-full h-full flex">
<div className="m-auto">{open && <BanduLoginForm />}</div>
</div>

View File

@@ -8,12 +8,6 @@ import { t } from "i18next";
export const DbState = () => {
const db = useContext(DbProviderContext);
useEffect(() => {
if (db.state === "disconnected") {
db.connect();
}
}, [db.state]);
if (db.state === "connecting") {
return (
<div className="flex justify-center">

View File

@@ -25,7 +25,7 @@ export const Layout = () => {
</div>
<div className="">
<Link to="/landing" replace>
<Link data-testid="landing-button" to="/landing" replace>
<Button size="lg">{t("startToUse")}</Button>
</Link>
</div>

View File

@@ -2,13 +2,10 @@ import {
Separator,
Card,
CardContent,
CardHeader,
CardTitle,
Avatar,
AvatarImage,
AvatarFallback,
Button,
toast,
Tabs,
TabsList,
TabsTrigger,
@@ -28,40 +25,16 @@ import {
NetworkState,
} from "@renderer/components";
import { EmailLoginForm } from "./email-login-form";
import { Client } from "@/api";
export const LoginForm = () => {
const { user, EnjoyApp, login, apiUrl } = useContext(
AppSettingsProviderContext
);
const [rememberedUser, setRememberedUser] = useState(null);
const loginWithRememberedUser = async () => {
if (!rememberedUser) return;
const client = new Client({
baseUrl: apiUrl,
accessToken: rememberedUser.accessToken,
});
client
.me()
.then((user) => {
if (user?.id) {
login(Object.assign({}, rememberedUser, user));
}
})
.catch((error) => {
toast.error(error.message);
});
};
const { user, EnjoyApp, login } = useContext(AppSettingsProviderContext);
const [rememberedUsers, setRememberedUsers] = useState<{ id: string }[]>([]);
useEffect(() => {
if (user) return;
EnjoyApp.settings.getUser().then((user) => {
setRememberedUser(user);
EnjoyApp.appSettings.getSessions().then((sessions) => {
setRememberedUsers(sessions);
});
}, [user]);
}, []);
if (user) {
return (
@@ -73,77 +46,67 @@ export const LoginForm = () => {
);
}
if (rememberedUser) {
return (
<Tabs className="w-full max-w-md" defaultValue="login">
<TabsList className="w-full grid grid-cols-2">
<TabsTrigger value="login">{t("login")}</TabsTrigger>
<TabsTrigger value="advanced">{t("advanced")}</TabsTrigger>
</TabsList>
<TabsContent value="login">
<div className="px-4 py-2 border rounded-lg w-full max-w-md">
<div className="flex items-start justify-between py-4">
<div className="">
<div className="flex items-center space-x-2">
<Avatar>
<AvatarImage
crossOrigin="anonymous"
src={rememberedUser.avatarUrl}
/>
<AvatarFallback className="text-xl">
{rememberedUser.name[0].toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="">
<div className="text-sm font-semibold">
{rememberedUser.name}
</div>
<div className="text-xs text-muted-foreground">
{rememberedUser.id}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<Button
variant="secondary"
size="sm"
onClick={() => setRememberedUser(null)}
>
{t("reLogin")}
</Button>
<Button
variant="default"
size="sm"
onClick={loginWithRememberedUser}
>
{t("login")}
</Button>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="advanced">
<Card className="w-full max-w-md">
<CardContent className="mt-6">
<ApiUrlSettings />
<Separator />
<ProxySettings />
<Separator />
<NetworkState />
</CardContent>
</Card>
</TabsContent>
</Tabs>
);
}
return (
<Tabs className="w-full max-w-md" defaultValue="login">
<TabsList className="w-full grid grid-cols-2">
<Tabs
className="w-full max-w-md"
defaultValue={rememberedUsers.length > 0 ? "selectUser" : "login"}
>
<TabsList
className={`w-full grid grid-cols-${
rememberedUsers.length > 0 ? 3 : 2
}`}
>
{rememberedUsers.length > 0 && (
<TabsTrigger value="selectUser">{t("selectUser")}</TabsTrigger>
)}
<TabsTrigger value="login">{t("login")}</TabsTrigger>
<TabsTrigger value="advanced">{t("advanced")}</TabsTrigger>
</TabsList>
{rememberedUsers.length > 0 && (
<TabsContent value="selectUser">
<div className="grid gap-4 border rounded-lg">
{rememberedUsers.map((rememberedUser) => (
<div
key={rememberedUser.id}
className="px-4 py-2 border-b last:border-b-0 w-full max-w-md"
>
<div className="flex items-center justify-between py-4">
<div className="">
<div className="flex items-center space-x-2">
<Avatar>
<AvatarImage
crossOrigin="anonymous"
src={`https://api.dicebear.com/9.x/thumbs/svg?seed=${rememberedUser.id}`}
/>
<AvatarFallback className="text-xl"></AvatarFallback>
</Avatar>
<div className="">
<div className="text-sm font-semibold"></div>
<div className="text-xs text-muted-foreground">
{rememberedUser.id}
</div>
</div>
</div>
</div>
<div className="">
<Button
variant="default"
data-testid="login-with-remembered-user-button"
size="sm"
onClick={async () => {
await EnjoyApp.appSettings.setUser(rememberedUser);
login(rememberedUser);
}}
>
{t("select")}
</Button>
</div>
</div>
</div>
))}
</div>
</TabsContent>
)}
<TabsContent value="login">
<Card className="w-full max-w-md">
<CardContent className="mt-6">

View File

@@ -6,6 +6,8 @@ import {
Sheet,
SheetTrigger,
SheetContent,
SheetHeader,
SheetTitle,
} from "@renderer/components/ui";
import { useContext, useEffect, useState } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
@@ -37,6 +39,9 @@ export const MixinLoginButton = () => {
className="h-screen"
aria-describedby={undefined}
>
<SheetHeader>
<SheetTitle className="sr-only">Mixin Messenger Login</SheetTitle>
</SheetHeader>
<div className="w-full h-full flex">
<div className="m-auto">{open && <MixinLoginForm />}</div>
</div>
@@ -141,7 +146,7 @@ export const MixinLoginForm = () => {
setCountdown(120);
})
.catch((err) => {
toast.error(err.message);
toast.error(err.response?.data || err.message);
})
.finally(() => {
setLoading(false);
@@ -164,7 +169,7 @@ export const MixinLoginForm = () => {
if (user?.id && user?.accessToken) login(user);
})
.catch((err) => {
toast.error(err.message);
toast.error(err.response?.data || err.message);
});
}}
>

View File

@@ -47,9 +47,15 @@ export const Sidebar = () => {
const { EnjoyApp, cable } = useContext(AppSettingsProviderContext);
useEffect(() => {
if (!cable) return;
const channel = new NoticiationsChannel(cable);
channel.subscribe();
}, []);
return () => {
channel.unsubscribe();
};
}, [cable]);
return (
<div
@@ -190,7 +196,10 @@ export const Sidebar = () => {
</div>
</DialogTrigger>
<DialogContent className="max-w-screen-md xl:max-w-screen-lg h-5/6 p-0">
<DialogContent
aria-describedby={undefined}
className="max-w-screen-md xl:max-w-screen-lg h-5/6 p-0"
>
<DialogTitle className="hidden">
{t("sidebar.preferences")}
</DialogTitle>

View File

@@ -42,7 +42,7 @@ export const ApiUrlSettings = () => {
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex items-start justify-between py-4">
<div className="flex items-start justify-between space-x-2 py-4">
<div className="">
<div className="mb-2">{t("apiSettings")}</div>
<div className="text-sm text-muted-foreground mb-2 ml-1">
@@ -100,7 +100,7 @@ export const ApiUrlSettings = () => {
</Button>
)}
</div>
<div className="text-xs text-muted-foreground">
<div className="text-xs text-muted-foreground text-right">
<InfoIcon className="mr-1 w-3 h-3 inline" />
<span>{t("reloadIsNeededAfterChanged")}</span>
</div>

View File

@@ -13,7 +13,7 @@ export * from "./learning-language-settings";
export * from "./default-engine-settings";
export * from "./openai-settings";
export * from "./library-settings";
export * from "./whisper-settings";
export * from "./stt-settings";
export * from "./user-settings";
export * from "./email-settings";

View File

@@ -25,8 +25,8 @@ export const LibrarySettings = () => {
});
if (filePaths) {
EnjoyApp.settings.setLibrary(filePaths[0]);
const _library = await EnjoyApp.settings.getLibrary();
EnjoyApp.appSettings.setLibrary(filePaths[0]);
const _library = await EnjoyApp.appSettings.getLibrary();
if (_library !== libraryPath) {
EnjoyApp.app.relaunch();
}
@@ -83,9 +83,13 @@ const DiskUsage = () => {
const [usage, setUsage] = useState<DiskUsageType>([]);
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const openPath = async (path: string) => {
if (path) {
await EnjoyApp.shell.openPath(path);
const openPath = async (filePath: string) => {
console.log(filePath);
if (filePath?.match(/.+\.json$/)) {
await EnjoyApp.shell.openPath(filePath.split("/").slice(0, -1).join("/"));
} else if (filePath) {
await EnjoyApp.shell.openPath(filePath);
}
};

View File

@@ -21,11 +21,11 @@ import {
} from "@renderer/context";
import { useContext, useEffect, useState } from "react";
import { InfoIcon, AlertCircleIcon } from "lucide-react";
import { SttEngineOptionEnum } from "@/types/enums";
export const WhisperSettings = () => {
const { whisperConfig, refreshWhisperConfig, setWhisperService } = useContext(
AISettingsProviderContext
);
const { sttEngine, whisperConfig, refreshWhisperConfig, setSttEngine } =
useContext(AISettingsProviderContext);
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const [stderr, setStderr] = useState("");
@@ -58,39 +58,47 @@ export const WhisperSettings = () => {
<div className="">
<div className="flex items-center mb-2">
<span>{t("sttAiService")}</span>
{stderr && <AlertCircleIcon className="ml-2 w-4 h-4 text-yellow-500" />}
{stderr && (
<AlertCircleIcon className="ml-2 w-4 h-4 text-yellow-500" />
)}
</div>
<div className="text-sm text-muted-foreground">
{whisperConfig?.service === "local" &&
{sttEngine === SttEngineOptionEnum.LOCAL &&
t("localSpeechToTextDescription")}
{whisperConfig?.service === "azure" &&
t("azureSpeechToTextDescription")}
{whisperConfig?.service === "cloudflare" &&
t("cloudflareSpeechToTextDescription")}
{whisperConfig?.service === "openai" &&
{sttEngine === SttEngineOptionEnum.ENJOY_AZURE &&
t("enjoyAzureSpeechToTextDescription")}
{sttEngine === SttEngineOptionEnum.ENJOY_CLOUDFLARE &&
t("enjoyCloudflareSpeechToTextDescription")}
{sttEngine === SttEngineOptionEnum.OPENAI &&
t("openaiSpeechToTextDescription")}
</div>
</div>
<div className="flex items-center space-x-2">
<Select
value={whisperConfig.service}
value={sttEngine}
onValueChange={(value) => {
setWhisperService(value);
setSttEngine(value);
}}
>
<SelectTrigger className="min-w-fit">
<SelectValue placeholder="service"></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="local">{t("local")}</SelectItem>
<SelectItem value="azure">{t("azureAi")}</SelectItem>
<SelectItem value="cloudflare">{t("cloudflareAi")}</SelectItem>
<SelectItem value="openai">OpenAI</SelectItem>
<SelectItem value={SttEngineOptionEnum.LOCAL}>
{t("local")}
</SelectItem>
<SelectItem value={SttEngineOptionEnum.ENJOY_AZURE}>
{t("enjoyAzure")}
</SelectItem>
<SelectItem value={SttEngineOptionEnum.ENJOY_CLOUDFLARE}>
{t("enjoyCloudflare")}
</SelectItem>
<SelectItem value={SttEngineOptionEnum.OPENAI}>OpenAI</SelectItem>
</SelectContent>
</Select>
{whisperConfig.service === "local" && (
{sttEngine === "local" && (
<>
<Button onClick={handleCheck} variant="secondary" size="sm">
{t("check")}
@@ -119,7 +127,7 @@ export const WhisperSettings = () => {
</span>
<Button
onClick={() => {
EnjoyApp.shell.openPath(whisperConfig.modelsPath);
EnjoyApp.shell.openPath(whisperConfig?.modelsPath);
}}
variant="outline"
size="sm"

View File

@@ -127,7 +127,7 @@ export const UserSettings = () => {
className="bg-destructive hover:bg-destructive-hover"
onClick={() => {
logout();
redirect("/");
redirect("/landing");
}}
>
{t("logout")}

View File

@@ -59,13 +59,13 @@ export const TranscriptionCreateForm = (props: {
originalText,
} = props;
const { learningLanguage } = useContext(AppSettingsProviderContext);
const { whisperConfig } = useContext(AISettingsProviderContext);
const { sttEngine } = useContext(AISettingsProviderContext);
const form = useForm<z.infer<typeof transcriptionSchema>>({
resolver: zodResolver(transcriptionSchema),
values: {
language: learningLanguage,
service: originalText ? "upload" : whisperConfig.service,
service: originalText ? "upload" : sttEngine,
text: originalText,
isolate: false,
},
@@ -166,9 +166,9 @@ export const TranscriptionCreateForm = (props: {
</SelectTrigger>
<SelectContent>
<SelectItem value="local">{t("local")}</SelectItem>
<SelectItem value="azure">{t("azureAi")}</SelectItem>
<SelectItem value="azure">{t("enjoyAzure")}</SelectItem>
<SelectItem value="cloudflare">
{t("cloudflareAi")}
{t("enjoyCloudflare")}
</SelectItem>
<SelectItem value="openai">OpenAI</SelectItem>
<SelectItem value="upload">{t("upload")}</SelectItem>
@@ -178,9 +178,9 @@ export const TranscriptionCreateForm = (props: {
{form.watch("service") === "local" &&
t("localSpeechToTextDescription")}
{form.watch("service") === "azure" &&
t("azureSpeechToTextDescription")}
t("enjoyAzureSpeechToTextDescription")}
{form.watch("service") === "cloudflare" &&
t("cloudflareSpeechToTextDescription")}
t("enjoyCloudflareSpeechToTextDescription")}
{form.watch("service") === "openai" &&
t("openaiSpeechToTextDescription")}
{form.watch("service") === "upload" &&
@@ -324,7 +324,12 @@ export const TranscriptionCreateForm = (props: {
{t("cancel")}
</Button>
)}
<Button disabled={transcribing} type="submit" variant="default">
<Button
data-testid="transcribe-continue-button"
disabled={transcribing}
type="submit"
variant="default"
>
{transcribing && <LoaderIcon className="animate-spin w-4 mr-2" />}
{t("continue")}
</Button>

View File

@@ -7,10 +7,10 @@ export const Sentence = ({ sentence }: { sentence: string }) => {
<span className="break-all align-middle">
{words.map((word, index) => {
return (
<>
<span key={index}>
<Vocabulary key={index} word={word} context={sentence} />
{index === words.length - 1 ? " " : " "}
</>
</span>
);
})}
</span>

View File

@@ -1,9 +1,14 @@
import { createContext, useEffect, useState, useContext } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
import {
AppSettingsProviderContext,
DbProviderContext,
} from "@renderer/context";
import { SttEngineOptionEnum, UserSettingKeyEnum } from "@/types/enums";
type AISettingsProviderState = {
setWhisperModel?: (name: string) => Promise<void>;
setWhisperService?: (name: string) => Promise<void>;
sttEngine?: SttEngineOptionEnum;
setSttEngine?: (name: string) => Promise<void>;
whisperConfig?: WhisperConfigType;
refreshWhisperConfig?: () => void;
openai?: LlmProviderType;
@@ -30,19 +35,26 @@ export const AISettingsProvider = ({
});
const [openai, setOpenai] = useState<LlmProviderType>(null);
const [whisperConfig, setWhisperConfig] = useState<WhisperConfigType>(null);
const [sttEngine, setSttEngine] = useState<SttEngineOptionEnum>(
SttEngineOptionEnum.ENJOY_AZURE
);
const { EnjoyApp, libraryPath, user, apiUrl } = useContext(
AppSettingsProviderContext
);
const db = useContext(DbProviderContext);
useEffect(() => {
if (db.state !== "connected") return;
fetchSettings();
}, []);
}, [db.state]);
useEffect(() => {
if (db.state !== "connected") return;
if (!libraryPath) return;
refreshWhisperConfig();
}, [libraryPath]);
}, [db.state, libraryPath]);
const refreshWhisperConfig = async () => {
const config = await EnjoyApp.whisper.config();
@@ -56,34 +68,29 @@ export const AISettingsProvider = ({
});
};
const setWhisperService = async (name: WhisperConfigType["service"]) => {
return EnjoyApp.whisper.setService(name).then((config) => {
if (!config) return;
setWhisperConfig(config);
});
const handleSetSttEngine = async (name: SttEngineOptionEnum) => {
setSttEngine(name);
return EnjoyApp.userSettings.set(UserSettingKeyEnum.STT_ENGINE, name);
};
const fetchSettings = async () => {
const _openai = await EnjoyApp.settings.getLlm("openai");
const _sttEngine = await EnjoyApp.userSettings.get(
UserSettingKeyEnum.STT_ENGINE
);
if (_sttEngine) {
setSttEngine(_sttEngine);
}
const _openai = await EnjoyApp.userSettings.get(UserSettingKeyEnum.OPENAI);
if (_openai) {
setOpenai(Object.assign({ name: "openai" }, _openai));
}
const _defaultEngine = await EnjoyApp.settings.getDefaultEngine();
const _gptEngine = await EnjoyApp.settings.getGptEngine();
const _gptEngine = await EnjoyApp.userSettings.get(
UserSettingKeyEnum.GPT_ENGINE
);
if (_gptEngine) {
setGptEngine(_gptEngine);
} else if (_defaultEngine) {
// Migrate default engine to gpt engine
const engine = {
name: _defaultEngine,
models: {
default: "gpt-4o",
},
};
EnjoyApp.settings.setGptEngine(engine).then(() => {
setGptEngine(engine);
});
} else if (_openai?.key) {
const engine = {
name: "openai",
@@ -91,9 +98,11 @@ export const AISettingsProvider = ({
default: "gpt-4o",
},
};
EnjoyApp.settings.setGptEngine(engine).then(() => {
setGptEngine(engine);
});
EnjoyApp.userSettings
.set(UserSettingKeyEnum.GPT_ENGINE, engine)
.then(() => {
setGptEngine(engine);
});
} else {
const engine = {
name: "enjoyai",
@@ -101,35 +110,28 @@ export const AISettingsProvider = ({
default: "gpt-4o",
},
};
EnjoyApp.settings.setGptEngine(engine).then(() => {
setGptEngine(engine);
});
EnjoyApp.userSettings
.set(UserSettingKeyEnum.GPT_ENGINE, engine)
.then(() => {
setGptEngine(engine);
});
}
};
const handleSetLlm = async (
name: SupportedLlmProviderType,
config: LlmProviderType
) => {
await EnjoyApp.settings.setLlm(name, config);
const _config = await EnjoyApp.settings.getLlm(name);
switch (name) {
case "openai":
setOpenai(Object.assign({ name: "openai" }, _config));
break;
default:
throw new Error("Unsupported LLM provider");
}
const handleSetOpenai = async (config: LlmProviderType) => {
await EnjoyApp.userSettings.set(UserSettingKeyEnum.OPENAI, config);
setOpenai(Object.assign({ name: "openai" }, config));
};
return (
<AISettingsProviderContext.Provider
value={{
setGptEngine: (engine: GptEngineSettingType) => {
EnjoyApp.settings.setGptEngine(engine).then(() => {
setGptEngine(engine);
});
EnjoyApp.userSettings
.set(UserSettingKeyEnum.GPT_ENGINE, engine)
.then(() => {
setGptEngine(engine);
});
},
currentEngine:
gptEngine.name === "openai"
@@ -142,11 +144,12 @@ export const AISettingsProvider = ({
baseUrl: `${apiUrl}/api/ai`,
}),
openai,
setOpenai: (config: LlmProviderType) => handleSetLlm("openai", config),
setOpenai: (config: LlmProviderType) => handleSetOpenai(config),
whisperConfig,
refreshWhisperConfig,
setWhisperModel,
setWhisperService,
sttEngine,
setSttEngine: (name: SttEngineOptionEnum) => handleSetSttEngine(name),
}}
>
{children}

View File

@@ -1,4 +1,4 @@
import { createContext, useEffect, useState } from "react";
import { createContext, useContext, useEffect, useState } from "react";
import { WEB_API_URL, LANGUAGES, IPA_MAPPINGS } from "@/constants";
import { Client } from "@/api";
import i18n from "@renderer/i18n";
@@ -6,6 +6,8 @@ import ahoy from "ahoy.js";
import { type Consumer, createConsumer } from "@rails/actioncable";
import * as Sentry from "@sentry/electron/renderer";
import { SENTRY_DSN } from "@/constants";
import { DbProviderContext } from "@renderer/context";
import { UserSettingKeyEnum } from "@/types/enums";
type AppSettingsProviderState = {
webApi: Client;
@@ -68,6 +70,7 @@ export const AppSettingsProvider = ({
const [ipaMappings, setIpaMappings] = useState<{ [key: string]: string }>(
IPA_MAPPINGS
);
const db = useContext(DbProviderContext);
const initSentry = () => {
EnjoyApp.app.isPackaged().then((isPackaged) => {
@@ -80,24 +83,30 @@ export const AppSettingsProvider = ({
};
const fetchLanguages = async () => {
const language = await EnjoyApp.settings.getLanguage();
setLanguage(language as "en" | "zh-CN");
const language = await EnjoyApp.userSettings.get(
UserSettingKeyEnum.LANGUAGE
);
setLanguage((language as "en" | "zh-CN") || "en");
i18n.changeLanguage(language);
const _nativeLanguage =
(await EnjoyApp.settings.get("nativeLanguage")) || "zh-CN";
(await EnjoyApp.userSettings.get(UserSettingKeyEnum.NATIVE_LANGUAGE)) ||
"zh-CN";
setNativeLanguage(_nativeLanguage);
const _learningLanguage =
(await EnjoyApp.settings.get("learningLanguage")) || "en-US";
(await EnjoyApp.userSettings.get(UserSettingKeyEnum.LEARNING_LANGUAGE)) ||
"en-US";
setLearningLanguage(_learningLanguage);
};
const switchLanguage = (language: "en" | "zh-CN") => {
EnjoyApp.settings.switchLanguage(language).then(() => {
i18n.changeLanguage(language);
setLanguage(language);
});
EnjoyApp.userSettings
.set(UserSettingKeyEnum.LANGUAGE, language)
.then(() => {
i18n.changeLanguage(language);
setLanguage(language);
});
};
const switchNativeLanguage = (lang: string) => {
@@ -105,14 +114,14 @@ export const AppSettingsProvider = ({
if (lang == learningLanguage) return;
setNativeLanguage(lang);
EnjoyApp.settings.set("nativeLanguage", lang);
EnjoyApp.userSettings.set(UserSettingKeyEnum.NATIVE_LANGUAGE, lang);
};
const switchLearningLanguage = (lang: string) => {
if (LANGUAGES.findIndex((l) => l.code == lang) < 0) return;
if (lang == nativeLanguage) return;
EnjoyApp.settings.set("learningLanguage", lang);
EnjoyApp.userSettings.set(UserSettingKeyEnum.LEARNING_LANGUAGE, lang);
setLearningLanguage(lang);
};
@@ -121,43 +130,40 @@ export const AppSettingsProvider = ({
setVersion(version);
};
const fetchUser = async () => {
const fetchApiUrl = async () => {
const apiUrl = await EnjoyApp.app.apiUrl();
setApiUrl(apiUrl);
const currentUser = await EnjoyApp.settings.getUser();
if (!currentUser) return;
const client = new Client({
baseUrl: apiUrl,
accessToken: currentUser.accessToken,
});
client.me().then((user) => {
if (user?.id) {
login(Object.assign({}, currentUser, user));
}
});
};
const login = (user: UserType) => {
const autoLogin = async () => {
const currentUser = await EnjoyApp.appSettings.getUser();
if (!currentUser) return;
setUser(currentUser);
};
const login = async (user: UserType) => {
if (!user?.id) return;
setUser(user);
EnjoyApp.settings.setUser(user);
createCable(user.accessToken);
if (user.accessToken) {
// Set current user to App settings
EnjoyApp.appSettings.setUser({ id: user.id, name: user.name });
}
};
const logout = () => {
setUser(null);
EnjoyApp.settings.setUser(null);
EnjoyApp.appSettings.setUser(null);
};
const fetchLibraryPath = async () => {
const dir = await EnjoyApp.settings.getLibrary();
const dir = await EnjoyApp.appSettings.getLibrary();
setLibraryPath(dir);
};
const setLibraryPathHandler = async (dir: string) => {
await EnjoyApp.settings.setLibrary(dir);
await EnjoyApp.appSettings.setLibrary(dir);
setLibraryPath(dir);
};
@@ -173,19 +179,21 @@ export const AppSettingsProvider = ({
};
const setApiUrlHandler = async (url: string) => {
EnjoyApp.settings.setApiUrl(url).then(() => {
EnjoyApp.appSettings.setApiUrl(url).then(() => {
EnjoyApp.app.reload();
});
};
const createCable = async (token: string) => {
if (!token) return;
const wsUrl = await EnjoyApp.app.wsUrl();
const consumer = createConsumer(wsUrl + "/cable?token=" + token);
setCable(consumer);
};
const fetchRecorderConfig = async () => {
const config = await EnjoyApp.settings.get("recorderConfig");
const config = await EnjoyApp.userSettings.get(UserSettingKeyEnum.RECORDER);
if (config) {
setRecorderConfig(config);
} else {
@@ -201,30 +209,45 @@ export const AppSettingsProvider = ({
};
const setRecorderConfigHandler = async (config: RecorderConfigType) => {
return EnjoyApp.settings.set("recorderConfig", config).then(() => {
setRecorderConfig(config);
});
return EnjoyApp.userSettings
.set(UserSettingKeyEnum.RECORDER, config)
.then(() => {
setRecorderConfig(config);
});
};
const fetchVocabularyConfig = async () => {
const config = await EnjoyApp.settings.getVocabularyConfig();
setVocabularyConfig(config || { lookupOnMouseOver: false });
EnjoyApp.userSettings
.get(UserSettingKeyEnum.VOCABULARY)
.then((config) => {
setVocabularyConfig(config || { lookupOnMouseOver: true });
})
.catch((err) => {
console.error(err);
setVocabularyConfig({ lookupOnMouseOver: true });
});
};
const setVocabularyConfigHandler = async (config: VocabularyConfigType) => {
await EnjoyApp.settings.setVocabularyConfig(config);
await EnjoyApp.userSettings.set(UserSettingKeyEnum.VOCABULARY, config);
setVocabularyConfig(config);
};
useEffect(() => {
fetchVersion();
fetchUser();
fetchLibraryPath();
if (db.state !== "connected") return;
fetchLanguages();
fetchProxyConfig();
fetchVocabularyConfig();
initSentry();
fetchRecorderConfig();
}, [db.state]);
useEffect(() => {
autoLogin();
fetchVersion();
fetchLibraryPath();
fetchProxyConfig();
fetchApiUrl();
}, []);
useEffect(() => {
@@ -235,6 +258,11 @@ export const AppSettingsProvider = ({
baseUrl: apiUrl,
accessToken: user?.accessToken,
locale: language,
onError: (err) => {
if (user.accessToken && err.status == 401) {
setUser({ ...user, accessToken: null });
}
},
})
);
}, [user, apiUrl, language]);
@@ -255,6 +283,27 @@ export const AppSettingsProvider = ({
});
}, [webApi]);
useEffect(() => {
if (!user) return;
db.connect().then(async () => {
// Login via API, update profile to DB
if (user.accessToken) {
EnjoyApp.userSettings.set(UserSettingKeyEnum.PROFILE, user);
} else {
// Auto login from local settings, get full profile from DB
const profile = await EnjoyApp.userSettings.get(
UserSettingKeyEnum.PROFILE
);
setUser(profile);
EnjoyApp.appSettings.setUser({ id: profile.id, name: profile.name });
}
});
return () => {
db.disconnect();
};
}, [user?.id]);
return (
<AppSettingsProviderContext.Provider
value={{

View File

@@ -1,19 +1,16 @@
import { createContext, useState, useEffect, useContext } from "react";
import { AppSettingsProviderContext } from "./app-settings-provider";
import log from "electron-log/renderer";
type DbStateEnum = "connected" | "connecting" | "error" | "disconnected";
type DbState = {
type DbProviderState = {
state: DbStateEnum;
path?: string;
error?: string;
connect?: () => void;
connect?: () => Promise<void>;
disconnect?: () => Promise<void>;
addDblistener?: (callback: (event: CustomEvent) => void) => void;
removeDbListener?: (callback: (event: CustomEvent) => void) => void;
};
type DbProviderState = DbState & {
connect?: () => void;
};
const initialState: DbProviderState = {
state: "disconnected",
@@ -25,20 +22,44 @@ export const DbProvider = ({ children }: { children: React.ReactNode }) => {
const [state, setState] = useState<DbStateEnum>("disconnected");
const [path, setPath] = useState();
const [error, setError] = useState();
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const EnjoyApp = window.__ENJOY_APP__;
const connect = async () => {
if (["connected", "connecting"].includes(state)) return;
console.info("--- connecting db ---");
setState("connecting");
const _db = await EnjoyApp.db.init();
setState(_db.state);
setPath(_db.path);
setError(_db.error);
return EnjoyApp.db
.connect()
.then((_db) => {
setState(_db.state);
setPath(_db.path);
setError(_db.error);
})
.catch((err) => {
setState("error");
setError(err.message);
});
};
const disconnect = () => {
console.info("--- disconnecting db ---");
return EnjoyApp.db.disconnect().then(() => {
setState("disconnected");
setPath(undefined);
setError(undefined);
});
};
useEffect(() => {
console.info(
"--- db state changed ---\n",
`state: ${state};\n`,
`path: ${path};\n`,
`error: ${error};\n`
);
}, [state]);
const addDblistener = (callback: (event: CustomEvent) => void) => {
document.addEventListener("db-on-transaction", callback);
};
@@ -48,14 +69,14 @@ export const DbProvider = ({ children }: { children: React.ReactNode }) => {
};
useEffect(() => {
if (state !== "connected") return;
if (state === "connected") {
EnjoyApp.db.onTransaction((_event, state) => {
log.debug("db-on-transaction", state);
EnjoyApp.db.onTransaction((_event, state) => {
log.debug("db-on-transaction", state);
const event = new CustomEvent("db-on-transaction", { detail: state });
document.dispatchEvent(event);
});
const event = new CustomEvent("db-on-transaction", { detail: state });
document.dispatchEvent(event);
});
}
return () => {
EnjoyApp.db.removeListeners();
@@ -69,6 +90,7 @@ export const DbProvider = ({ children }: { children: React.ReactNode }) => {
path,
error,
connect,
disconnect,
addDblistener,
removeDbListener,
}}

View File

@@ -1,6 +1,10 @@
import { createContext, useState, useEffect, useContext, useMemo } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
import {
AppSettingsProviderContext,
DbProviderContext,
} from "@renderer/context";
import { t } from "i18next";
import { UserSettingKeyEnum } from "@/types/enums";
type DictProviderState = {
settings: DictSettingType;
@@ -48,6 +52,7 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
});
const [currentDictValue, setCurrentDictValue] = useState<string>("");
const [currentDict, setCurrentDict] = useState<Dict | null>();
const { state: dbState } = useContext(DbProviderContext);
const availableDicts = useMemo(
() =>
@@ -93,12 +98,14 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
}, [availableDicts, settings]);
useEffect(() => {
if (dbState !== "connected") return;
fetchSettings();
fetchDicts();
}, []);
}, [dbState]);
const fetchSettings = async () => {
return EnjoyApp.settings.getDictSettings().then((res) => {
return EnjoyApp.userSettings.get(UserSettingKeyEnum.DICTS).then((res) => {
res && setSettings(res);
});
};
@@ -119,8 +126,8 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
const setDefault = async (dict: Dict | null) => {
const _settings = { ...settings, default: dict?.name ?? "" };
EnjoyApp.settings
.setDictSettings(_settings)
EnjoyApp.userSettings
.set(UserSettingKeyEnum.DICTS, _settings)
.then(() => setSettings(_settings));
};
@@ -129,8 +136,8 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
const removing = [...(settings.removing ?? []), dict.name];
const _settings = { ...settings, removing };
EnjoyApp.settings
.setDictSettings(_settings)
EnjoyApp.userSettings
.set(UserSettingKeyEnum.DICTS, _settings)
.then(() => setSettings(_settings));
}
};
@@ -140,8 +147,8 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
settings.removing?.filter((name) => name !== dict.name) ?? [];
const _settings = { ...settings, removing };
EnjoyApp.settings
.setDictSettings(_settings)
EnjoyApp.userSettings
.set(UserSettingKeyEnum.DICTS, _settings)
.then(() => setSettings(_settings));
};

View File

@@ -6,8 +6,12 @@ import {
useState,
} from "react";
import { useHotkeys, useRecordHotkeys } from "react-hotkeys-hook";
import { AppSettingsProviderContext } from "./app-settings-provider";
import _ from "lodash";
import {
AppSettingsProviderContext,
DbProviderContext,
} from "@renderer/context";
import isEmpty from "lodash/isEmpty";
import { UserSettingKeyEnum } from "@/types/enums";
function isShortcutValid(shortcut: string) {
const modifiers = ["ctrl", "alt", "shift", "meta"];
@@ -150,18 +154,25 @@ export const HotKeysSettingsProvider = ({
const [keys, { start, stop, resetKeys, isRecording }] = useRecordHotkeys();
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const { state: dbState } = useContext(DbProviderContext);
useEffect(() => {
if (dbState !== "connected") return;
fetchSettings();
}, []);
}, [dbState]);
const fetchSettings = async () => {
const _hotkeys = await EnjoyApp.settings.getDefaultHotkeys();
const _hotkeys = await EnjoyApp.userSettings.get(
UserSettingKeyEnum.HOTKEYS
);
// During version iterations, there may be added or removed keys.
const merged = mergeWithPreference(_hotkeys ?? {}, defaultKeyMap);
await EnjoyApp.settings.setDefaultHotkeys(merged).then(() => {
setCurrentHotkeys(merged);
});
await EnjoyApp.userSettings
.set(UserSettingKeyEnum.HOTKEYS, merged)
.then(() => {
setCurrentHotkeys(merged);
});
};
const changeHotkey = useCallback(
@@ -200,9 +211,11 @@ export const HotKeysSettingsProvider = ({
};
}
await EnjoyApp.settings.setDefaultHotkeys(newMap).then(() => {
setCurrentHotkeys(newMap);
});
await EnjoyApp.userSettings
.set(UserSettingKeyEnum.HOTKEYS, newMap)
.then(() => {
setCurrentHotkeys(newMap);
});
resetKeys();
},
[currentHotkeys]
@@ -230,7 +243,9 @@ export const HotKeysSettingsProvider = ({
changeHotkey,
}}
>
{_.isEmpty(currentHotkeys) ? null : (
{isEmpty(currentHotkeys) ? (
children
) : (
<HotKeysSettingsSystemSettings
{...{
currentHotkeys,

View File

@@ -18,6 +18,7 @@ import { Tooltip } from "react-tooltip";
import { debounce } from "lodash";
import { useAudioRecorder } from "react-audio-voice-recorder";
import { t } from "i18next";
import { SttEngineOptionEnum } from "@/types/enums";
const ONE_MINUTE = 60;
const TEN_MINUTES = 10 * ONE_MINUTE;
@@ -73,7 +74,7 @@ type MediaPlayerContextType = {
generateTranscription: (params?: {
originalText?: string;
language?: string;
service?: WhisperConfigType["service"] | "upload";
service?: SttEngineOptionEnum | "upload";
isolate?: boolean;
}) => Promise<void>;
transcribing: boolean;

View File

@@ -18,6 +18,7 @@ import {
import take from "lodash/take";
import sortedUniqBy from "lodash/sortedUniqBy";
import { parseText } from "media-captions";
import { SttEngineOptionEnum } from "@/types/enums";
// test a text string has any punctuations or not
// some transcribed text may not have any punctuations
@@ -48,7 +49,7 @@ export const useTranscribe = () => {
targetType?: string;
originalText?: string;
language: string;
service: WhisperConfigType["service"] | "upload";
service: SttEngineOptionEnum | "upload";
isolate?: boolean;
align?: boolean;
}
@@ -100,15 +101,15 @@ export const useTranscribe = () => {
text: originalText,
};
}
} else if (service === "local") {
} else if (service === SttEngineOptionEnum.LOCAL) {
result = await transcribeByLocal(url, language);
} else if (service === "cloudflare") {
} else if (service === SttEngineOptionEnum.ENJOY_CLOUDFLARE) {
result = await transcribeByCloudflareAi(blob);
} else if (service === "openai") {
} else if (service === SttEngineOptionEnum.OPENAI) {
result = await transcribeByOpenAi(
new File([blob], "audio.mp3", { type: "audio/mp3" })
);
} else if (service === "azure") {
} else if (service === SttEngineOptionEnum.ENJOY_AZURE) {
result = await transcribeByAzureAi(
new File([blob], "audio.wav", { type: "audio/wav" }),
language,

View File

@@ -8,21 +8,18 @@ import {
import { toast } from "@renderer/components/ui";
import { TimelineEntry } from "echogarden/dist/utilities/Timeline.d.js";
import { MAGIC_TOKEN_REGEX, END_OF_SENTENCE_REGEX } from "@/constants";
import { SttEngineOptionEnum } from "@/types/enums";
export const useTranscriptions = (media: AudioType | VideoType) => {
const { whisperConfig } = useContext(AISettingsProviderContext);
const { EnjoyApp, learningLanguage } = useContext(
AppSettingsProviderContext
);
const { sttEngine } = useContext(AISettingsProviderContext);
const { EnjoyApp, learningLanguage } = useContext(AppSettingsProviderContext);
const { addDblistener, removeDbListener } = useContext(DbProviderContext);
const [transcription, setTranscription] = useState<TranscriptionType>(null);
const { transcribe, output } = useTranscribe();
const [transcribingProgress, setTranscribingProgress] = useState<number>(0);
const [transcribing, setTranscribing] = useState<boolean>(false);
const [transcribingOutput, setTranscribingOutput] = useState<string>("");
const [service, setService] = useState<
WhisperConfigType["service"] | "upload"
>(whisperConfig.service);
const [service, setService] = useState<SttEngineOptionEnum | "upload">(sttEngine);
const onTransactionUpdate = (event: CustomEvent) => {
if (!transcription) return;
@@ -63,13 +60,13 @@ export const useTranscriptions = (media: AudioType | VideoType) => {
const generateTranscription = async (params?: {
originalText?: string;
language?: string;
service?: WhisperConfigType["service"] | "upload";
service?: SttEngineOptionEnum | "upload";
isolate?: boolean;
}) => {
let {
originalText,
language = learningLanguage,
service = whisperConfig.service,
service = sttEngine,
isolate = false,
} = params || {};
setService(service);

View File

@@ -5,10 +5,11 @@ import {
VideosSegment,
YoutubeVideosSegment,
EnrollmentSegment,
Vocabulary,
} from "@renderer/components";
import { useContext, useEffect, useState } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
import { Button } from "@renderer/components/ui";
import { t } from "i18next";
export default () => {
const [channels, setChannels] = useState<string[]>([
@@ -28,17 +29,37 @@ export default () => {
}, []);
return (
<div className="max-w-5xl mx-auto px-4 py-6 lg:px-8">
<div className="space-y-4">
<EnrollmentSegment />
<AudiosSegment />
<VideosSegment />
<StoriesSegment />
<AudibleBooksSegment />
{channels.map((channel) => (
<YoutubeVideosSegment key={channel} channel={channel} />
))}
<div className="relative">
<AuthorizationStatusBar />
<div className="max-w-5xl mx-auto px-4 py-6 lg:px-8">
<div className="space-y-4">
<EnrollmentSegment />
<AudiosSegment />
<VideosSegment />
<StoriesSegment />
<AudibleBooksSegment />
{channels.map((channel) => (
<YoutubeVideosSegment key={channel} channel={channel} />
))}
</div>
</div>
</div>
);
};
const AuthorizationStatusBar = () => {
const { user, logout } = useContext(AppSettingsProviderContext);
if (user.accessToken === null) {
return (
<div className="bg-red-500 text-white py-2 px-4 h-12 flex items-center sticky top-0">
<span className="text-sm">{t("authorizationExpired")}</span>
<Button variant="outline" size="sm" className="ml-2" onClick={logout}>
{t("reLogin")}
</Button>
</div>
);
}
return null;
};

View File

@@ -20,7 +20,7 @@ export default () => {
<div className="mt-auto">
<div className="flex mb-4 justify-end space-x-4">
{initialized && (
<Link to="/" replace>
<Link data-testid="start-to-use-button" to="/" replace>
<Button className="w-24">{t("startToUse")}</Button>
</Link>
)}

View File

@@ -115,7 +115,7 @@ type EnjoyAppType = {
) => Promise<Electron.MessageBoxReturnValue>;
showErrorBox: (title: string, content: string) => Promise<void>;
};
settings: {
appSettings: {
get: (key: string) => Promise<any>;
set: (key: string, value: any) => Promise<void>;
getLibrary: () => Promise<string>;
@@ -123,25 +123,13 @@ type EnjoyAppType = {
getUser: () => Promise<UserType>;
setUser: (user: UserType) => Promise<void>;
getUserDataPath: () => Promise<string>;
getDefaultEngine: () => Promise<string>;
setDefaultEngine: (string) => Promise<string>;
getGptEngine: () => Promise<GptEngineSettingType>;
setGptEngine: (GptEngineSettingType) => Promise<GptEngineSettingType>;
getLlm: (provider: SupportedLlmProviderType) => Promise<LlmProviderType>;
setLlm: (
provider: SupportedLlmProviderType,
LlmProviderType
) => Promise<void>;
getLanguage: () => Promise<string>;
switchLanguage: (language: string) => Promise<void>;
getDefaultHotkeys: () => Promise<Record<string, string> | undefined>;
setDefaultHotkeys: (records: Record<string, string>) => Promise<void>;
getDictSettings: () => Promise<DictSettingType>;
setDictSettings: (dict: DictSettingType) => Promise<void>;
getApiUrl: () => Promise<string>;
setApiUrl: (url: string) => Promise<void>;
getVocabularyConfig: () => Promise<VocabularyConfigType | undefined>;
setVocabularyConfig: (records: VocabularyConfigType) => Promise<void>;
getSessions: () => Promise<{ id: string }[]>;
};
userSettings: {
get: (key: UserSettingKeyEnum) => Promise<any>;
set: (key: UserSettingKeyEnum, value: any) => Promise<void>;
};
fs: {
ensureDir: (path: string) => Promise<boolean>;
@@ -150,7 +138,8 @@ type EnjoyAppType = {
join: (...paths: string[]) => Promise<string>;
};
db: {
init: () => Promise<DbState>;
connect: () => Promise<DbState>;
disconnect: () => Promise<void>;
onTransaction: (
callback: (event, state: TransactionStateType) => void
) => Promise<void>;
@@ -290,9 +279,6 @@ type EnjoyAppType = {
config: () => Promise<WhisperConfigType>;
check: () => Promise<{ success: boolean; log: string }>;
setModel: (model: string) => Promise<WhisperConfigType>;
setService: (
service: WhisperConfigType["service"]
) => Promise<WhisperConfigType>;
transcribe: (
params: {
file?: string;

27
enjoy/src/types/enums.ts Normal file
View File

@@ -0,0 +1,27 @@
export enum UserSettingKeyEnum {
PROFILE = "profile",
LANGUAGE = "language",
NATIVE_LANGUAGE = "native_language",
LEARNING_LANGUAGE = "learning_language",
WHISPER = "whisper",
OPENAI = "openai",
HOTKEYS = "hotkeys",
GPT_ENGINE = "gpt_engine",
STT_ENGINE = "stt_engine",
VOCABULARY = "vocabulary",
DICTS = "dicts",
RECORDER = "recorder",
}
export enum SttEngineOptionEnum {
LOCAL = "local",
ENJOY_AZURE = "enjoy_azure",
ENJOY_CLOUDFLARE = "enjoy_cloudflare",
OPENAI = "openai",
}
export enum AppSettingsKeyEnum {
LIBRARY = "library",
USER = "user",
API_URL = "api_url",
}

View File

@@ -40,7 +40,7 @@ type NotificationType = {
};
type WhisperConfigType = {
service: "local" | "azure" | "cloudflare" | "openai";
// service: "local" | "azure" | "cloudflare" | "openai";
availableModels: {
type: string;
name: string;

View File

@@ -1,6 +1,6 @@
type UserType = {
id: string;
name: string;
name?: string;
email?: string;
balance?: number;
avatarUrl?: string;

1415
yarn.lock

File diff suppressed because it is too large Load Diff