Improve: UI & code (#1179)
* use frameless * frame true * fix UI * disable whisper.cpp for darwin * improve page UI * clean code * refactor shadowing cancel * upgrade deps * fix type * update e2e * downgrade echogarden to fix align error * upgrade echogarden * upgrade * fix profile * refactor login form * may scan to login with Mixin * refactor sidebar * update sidebar ui * update * update UI * update
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
"markdown-it-sub": "^2.0.0",
|
||||
"markdown-it-sup": "^2.0.0",
|
||||
"mermaid": "^11.4.0",
|
||||
"sass": "^1.80.6",
|
||||
"sass": "^1.80.7",
|
||||
"vitepress": "^1.5.0",
|
||||
"vitepress-plugin-mermaid": "^2.0.17",
|
||||
"vue": "^3.5.12"
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.48",
|
||||
"sass": "^1.80.6",
|
||||
"postcss": "^8.4.49",
|
||||
"sass": "^1.80.7",
|
||||
"tailwindcss": "^3.4.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ test.afterAll(async () => {
|
||||
await electronApp.close();
|
||||
});
|
||||
|
||||
test("validate echogarden command", async () => {
|
||||
test("validate echogarden recognition", async () => {
|
||||
const res = await page.evaluate(() => {
|
||||
return window.__ENJOY_APP__.echogarden.check({
|
||||
engine: "whisper",
|
||||
|
||||
@@ -62,8 +62,8 @@
|
||||
"@types/unzipper": "^0.10.10",
|
||||
"@types/validator": "^13.12.2",
|
||||
"@types/wavesurfer.js": "^6.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
||||
"@typescript-eslint/parser": "^8.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.14.0",
|
||||
"@typescript-eslint/parser": "^8.14.0",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"electron": "^33.2.0",
|
||||
@@ -83,17 +83,17 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.10",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-static-copy": "^2.1.0",
|
||||
"zx": "^8.2.1"
|
||||
"zx": "^8.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@andrkrn/ffprobe-static": "^5.2.0",
|
||||
"@divisey/js-mdict": "^5.0.0",
|
||||
"@electron-forge/publisher-s3": "^7.5.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@langchain/community": "^0.3.12",
|
||||
"@langchain/core": "^0.3.17",
|
||||
"@langchain/community": "^0.3.14",
|
||||
"@langchain/core": "^0.3.18",
|
||||
"@langchain/ollama": "^0.1.2",
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"@radix-ui/react-accordion": "^1.2.1",
|
||||
@@ -110,7 +110,7 @@
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-progress": "^1.1.0",
|
||||
"@radix-ui/react-radio-group": "^1.2.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slider": "^1.2.1",
|
||||
@@ -119,7 +119,7 @@
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-toast": "^1.2.2",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@rails/actioncable": "8.0.0",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
@@ -141,7 +141,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"decamelize": "^6.0.0",
|
||||
"decamelize-keys": "^2.0.1",
|
||||
"echogarden": "^2.0.0",
|
||||
"echogarden": "2.0.3",
|
||||
"electron-context-menu": "^4.0.4",
|
||||
"electron-log": "^5.2.2",
|
||||
"electron-settings": "^4.0.4",
|
||||
@@ -154,6 +154,7 @@
|
||||
"html-to-text": "^9.0.5",
|
||||
"https-proxy-agent": "^7.0.5",
|
||||
"i18next": "^23.16.5",
|
||||
"input-otp": "^1.4.1",
|
||||
"intl-tel-input": "^24.7.0",
|
||||
"js-md5": "^0.8.3",
|
||||
"langchain": "^0.3.5",
|
||||
@@ -161,13 +162,14 @@
|
||||
"lru-cache": "^11.0.2",
|
||||
"lucide-react": "^0.456.0",
|
||||
"mark.js": "^8.11.1",
|
||||
"media-captions": "^0.0.18",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.41.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"mustache": "^4.2.0",
|
||||
"next-themes": "^0.4.3",
|
||||
"openai": "^4.71.1",
|
||||
"openai": "^4.72.0",
|
||||
"pitchfinder": "^2.3.2",
|
||||
"postcss": "^8.4.48",
|
||||
"postcss": "^8.4.49",
|
||||
"proxy-agent": "^6.4.0",
|
||||
"react": "^18.3.1",
|
||||
"react-activity-calendar": "^2.7.1",
|
||||
|
||||
@@ -106,6 +106,10 @@ export class Client {
|
||||
return this.api.post("/api/sessions", decamelizeKeys(params));
|
||||
}
|
||||
|
||||
oauthState(state: string): Promise<UserType> {
|
||||
return this.api.post("/api/sessions/oauth_state", { state });
|
||||
}
|
||||
|
||||
config(key: string): Promise<any> {
|
||||
return this.api.get(`/api/config/${key}`);
|
||||
}
|
||||
|
||||
@@ -275,7 +275,9 @@
|
||||
"phoneNumber": "Phone number",
|
||||
"mixinId": "Mixin ID",
|
||||
"inputMixinId": "Input your Mixin ID",
|
||||
"dontHaveMixinAccount": "don't have Mixin account?",
|
||||
"scanMixinQRCodeDescription": "Scan Mixin QR code in the popup window",
|
||||
"createMixinAccount": "Create Mixin account",
|
||||
"scanToLogin": "Scan to login",
|
||||
"youCanAlsoLoginWith": "You can also login with",
|
||||
"downloadTranscript": "Download transcript",
|
||||
"downloadTranscriptFromCloud": "Download transcript from cloud",
|
||||
|
||||
@@ -275,7 +275,9 @@
|
||||
"phoneNumber": "手机号",
|
||||
"mixinId": "Mixin 号",
|
||||
"inputMixinId": "请输入您的 Mixin ID",
|
||||
"dontHaveMixinAccount": "没有 Mixin 账号?",
|
||||
"scanMixinQRCodeDescription": "请在弹出窗口中用 Mixin 扫码",
|
||||
"createMixinAccount": "创建 Mixin 账号",
|
||||
"scanToLogin": "扫码登录",
|
||||
"youCanAlsoLoginWith": "您也可以使用以下方式登录",
|
||||
"downloadTranscript": "下载字幕",
|
||||
"downloadTranscriptFromCloud": "从云端下载字幕",
|
||||
|
||||
@@ -139,7 +139,11 @@ app.on("ready", async () => {
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on("window-all-closed", () => {
|
||||
app.quit();
|
||||
// Respect the OSX convention of having the application in memory even
|
||||
// after all windows have been closed
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
|
||||
@@ -24,9 +24,6 @@ import settings from "@main/settings";
|
||||
import fs from "fs-extra";
|
||||
import ffmpegPath from "ffmpeg-static";
|
||||
import { enjoyUrlToPath, pathToEnjoyUrl } from "./utils";
|
||||
import { UserSetting } from "./db/models";
|
||||
import { UserSettingKeyEnum } from "@/types/enums";
|
||||
import { WHISPER_MODELS } from "@/constants";
|
||||
|
||||
Echogarden.setGlobalOption(
|
||||
"ffmpegPath",
|
||||
@@ -107,13 +104,18 @@ class EchogardenWrapper {
|
||||
try {
|
||||
logger.info("check:", options);
|
||||
const result = await this.recognize(sampleFile, options);
|
||||
logger.info(result?.transcript);
|
||||
logger.info("transcript:", result?.transcript);
|
||||
fs.writeJsonSync(
|
||||
path.join(settings.cachePath(), "echogarden-check.json"),
|
||||
result,
|
||||
{ spaces: 2 }
|
||||
);
|
||||
|
||||
const timeline = await this.align(sampleFile, result.transcript, {
|
||||
language: "en",
|
||||
});
|
||||
logger.info("timeline:", !!timeline);
|
||||
|
||||
return { success: true, log: "" };
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
|
||||
@@ -509,7 +509,8 @@ ${log}
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
icon: "./assets/icon.png",
|
||||
icon:
|
||||
process.platform === "win32" ? "./assets/icon.ico" : "./assets/icon.png",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
minWidth: 800,
|
||||
|
||||
@@ -7,8 +7,9 @@ export const AudioPlayer = (props: {
|
||||
id?: string;
|
||||
md5?: string;
|
||||
segmentIndex?: number;
|
||||
onLoad?: (audio: AudioType) => void;
|
||||
}) => {
|
||||
const { id, md5, segmentIndex } = props;
|
||||
const { id, md5, segmentIndex, onLoad } = props;
|
||||
const { media, setMedia, setCurrentSegmentIndex, getCachedSegmentIndex } =
|
||||
useContext(MediaShadowProviderContext);
|
||||
|
||||
@@ -21,6 +22,7 @@ export const AudioPlayer = (props: {
|
||||
|
||||
useEffect(() => {
|
||||
setMedia(audio);
|
||||
onLoad?.(audio);
|
||||
}, [audio]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -42,9 +42,9 @@ export const ChatHeader = (props: {
|
||||
onClick={toggleSidePanel}
|
||||
>
|
||||
{sidePanelCollapsed ? (
|
||||
<ChevronsRightIcon className="w-5 h-5" />
|
||||
<ChevronsRightIcon className="size-4" />
|
||||
) : (
|
||||
<ChevronsLeftIcon className="w-5 h-5" />
|
||||
<ChevronsLeftIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
{chat.type === ChatTypeEnum.CONVERSATION && (
|
||||
@@ -59,7 +59,7 @@ export const ChatHeader = (props: {
|
||||
<Dialog open={displayChatForm} onOpenChange={setDisplayChatForm}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="absolute right-4">
|
||||
<SettingsIcon className="w-5 h-5" />
|
||||
<SettingsIcon className="size-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-screen-sm max-h-[70%] overflow-y-auto">
|
||||
|
||||
@@ -19,3 +19,4 @@ export * from "./transcriptions";
|
||||
export * from "./users";
|
||||
export * from "./videos";
|
||||
export * from "./widgets";
|
||||
export * from "./login";
|
||||
|
||||
@@ -8,12 +8,16 @@ import {
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@renderer/components/ui";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { t } from "i18next";
|
||||
import intlTelInput from "intl-tel-input/intlTelInputWithUtils";
|
||||
import "intl-tel-input/build/css/intlTelInput.css";
|
||||
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||
|
||||
export const BanduLoginButton = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -117,30 +121,38 @@ export const BanduLoginForm = () => {
|
||||
value={phoneNumber}
|
||||
onInput={validatePhone}
|
||||
onBlur={validatePhone}
|
||||
disabled={countdown > 0}
|
||||
className="border text-lg py-2 px-4 rounded w-80 dark:bg-background dark:text-foreground"
|
||||
ref={ref}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="verrificationCode">{t("verificationCode")}</Label>
|
||||
<Input
|
||||
id="verrificationCode"
|
||||
className="border py-2 h-10 px-4 rounded"
|
||||
type="text"
|
||||
minLength={5}
|
||||
maxLength={5}
|
||||
placeholder={t("verificationCode")}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{codeSent && (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="verrificationCode">{t("verificationCode")}</Label>
|
||||
<InputOTP
|
||||
id="verrificationCode"
|
||||
maxLength={5}
|
||||
value={code}
|
||||
pattern={REGEXP_ONLY_DIGITS}
|
||||
onChange={(value) => setCode(value)}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
className="w-full px-2"
|
||||
disabled={!phoneNumber || countdown > 0}
|
||||
onClick={() => {
|
||||
webApi
|
||||
@@ -1,7 +1,16 @@
|
||||
import { Button, toast, Input, Label } from "@renderer/components/ui";
|
||||
import {
|
||||
Button,
|
||||
toast,
|
||||
Input,
|
||||
Label,
|
||||
InputOTP,
|
||||
InputOTPSlot,
|
||||
InputOTPGroup,
|
||||
} from "@renderer/components/ui";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { t } from "i18next";
|
||||
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||
|
||||
export const EmailLoginForm = () => {
|
||||
const [email, setEmail] = useState<string>("");
|
||||
@@ -41,27 +50,33 @@ export const EmailLoginForm = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="code">{t("verificationCode")}</Label>
|
||||
<Input
|
||||
id="code"
|
||||
className="h-10"
|
||||
type="text"
|
||||
required
|
||||
minLength={5}
|
||||
maxLength={5}
|
||||
placeholder={t("verificationCode")}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{codeSent && (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="code">{t("verificationCode")}</Label>
|
||||
<InputOTP
|
||||
id="code"
|
||||
maxLength={5}
|
||||
value={code}
|
||||
pattern={REGEXP_ONLY_DIGITS}
|
||||
onChange={(value) => setCode(value)}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
className="w-full px-2"
|
||||
disabled={!email || countdown > 0}
|
||||
onClick={() => {
|
||||
webApi
|
||||
@@ -83,7 +98,7 @@ export const EmailLoginForm = () => {
|
||||
<Button
|
||||
variant="default"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
className="w-full px-2"
|
||||
disabled={!code || code.length < 5 || !email}
|
||||
onClick={() => {
|
||||
webApi
|
||||
5
enjoy/src/renderer/components/login/index.ts
Normal file
5
enjoy/src/renderer/components/login/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./mixin-login-form";
|
||||
export * from "./login-form";
|
||||
export * from "./email-login-form";
|
||||
export * from "./bandu-login-form";
|
||||
export * from "./github-login-form";
|
||||
@@ -68,9 +68,9 @@ export const LoginForm = () => {
|
||||
{rememberedUsers.map((rememberedUser) => (
|
||||
<div
|
||||
key={rememberedUser.id}
|
||||
className="px-4 py-2 border-b last:border-b-0 w-full max-w-md"
|
||||
className="px-4 border-b last:border-b-0 w-full max-w-md"
|
||||
>
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div className="">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar>
|
||||
@@ -8,11 +8,17 @@ import {
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
Separator,
|
||||
} from "@renderer/components/ui";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { t } from "i18next";
|
||||
import { LoaderIcon } from "lucide-react";
|
||||
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export const MixinLoginButton = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -57,7 +63,11 @@ export const MixinLoginForm = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [codeSent, setCodeSent] = useState<boolean>(false);
|
||||
const [countdown, setCountdown] = useState<number>(0);
|
||||
const { login, webApi, EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const { login, webApi, EnjoyApp, apiUrl } = useContext(
|
||||
AppSettingsProviderContext
|
||||
);
|
||||
const [state, setState] = useState<string>("");
|
||||
const [scanning, setScanning] = useState<boolean>(false);
|
||||
|
||||
const validateMixinId = (id: string) => {
|
||||
setInput(id);
|
||||
@@ -69,6 +79,39 @@ export const MixinLoginForm = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleScanToLogin = () => {
|
||||
const uuid = uuidv4();
|
||||
setState(uuid);
|
||||
EnjoyApp.shell.openExternal(
|
||||
`${apiUrl}/sessions/new?provider=mixin&state=${uuid}`
|
||||
);
|
||||
setScanning(true);
|
||||
};
|
||||
|
||||
const pollingOAuthState = () => {
|
||||
if (!state) return;
|
||||
|
||||
webApi.oauthState(state).then((user) => {
|
||||
if (user?.id && user?.accessToken) {
|
||||
login(user);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
if (!scanning) return;
|
||||
|
||||
interval = setInterval(() => {
|
||||
pollingOAuthState();
|
||||
}, 1500);
|
||||
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [scanning]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
@@ -83,10 +126,41 @@ export const MixinLoginForm = () => {
|
||||
};
|
||||
}, [countdown]);
|
||||
|
||||
if (scanning) {
|
||||
return (
|
||||
<div className="w-80">
|
||||
<div className="flex items-center justify-center mb-4">
|
||||
<img src="assets/mixin-logo.png" className="w-20 h-20" alt="mixin" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center mb-4">
|
||||
<LoaderIcon className="w-5 h-5 animate-spin" />
|
||||
</div>
|
||||
<div className="text-center text-sm text-muted-foreground mb-6">
|
||||
{t("scanMixinQRCodeDescription")}
|
||||
</div>
|
||||
<div className="flex justify-center items-center space-x-4">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() =>
|
||||
EnjoyApp.shell.openExternal(
|
||||
`${apiUrl}/sessions/new?provider=mixin&state=${state}`
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("open")}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => setScanning(false)}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-80">
|
||||
<div className="flex items-center justify-center mb-4">
|
||||
<img src="assets/mixin-logo.png" className="w-20 h-20" alt="bandu" />
|
||||
<img src="assets/mixin-logo.png" className="w-20 h-20" alt="mixin" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6">
|
||||
@@ -96,33 +170,50 @@ export const MixinLoginForm = () => {
|
||||
<input
|
||||
id="mixinId"
|
||||
value={input}
|
||||
disabled={countdown > 0}
|
||||
placeholder={t("inputMixinId")}
|
||||
onInput={(event) => validateMixinId(event.currentTarget.value)}
|
||||
onBlur={(event) => validateMixinId(event.currentTarget.value)}
|
||||
className="border py-2 px-4 rounded dark:bg-background dark:text-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="verificationCode">{t("verificationCode")}</Label>
|
||||
<Input
|
||||
id="verificationCode"
|
||||
className="border py-2 h-10 px-4 rounded"
|
||||
type="text"
|
||||
minLength={5}
|
||||
maxLength={5}
|
||||
placeholder={t("verificationCode")}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{codeSent && (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="verificationCode">{t("verificationCode")}</Label>
|
||||
<InputOTP
|
||||
id="verificationCode"
|
||||
maxLength={5}
|
||||
value={code}
|
||||
pattern={REGEXP_ONLY_DIGITS}
|
||||
onChange={(value) => setCode(value)}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
onClick={() =>
|
||||
EnjoyApp.shell.openExternal("https://mixin.one/messenger")
|
||||
}
|
||||
className="text-xs text-muted-foreground cursor-pointer"
|
||||
>
|
||||
{t("dontHaveMixinAccount")}
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
onClick={() =>
|
||||
EnjoyApp.shell.openExternal("https://mixin.one/messenger")
|
||||
}
|
||||
className="text-xs text-muted-foreground cursor-pointer"
|
||||
>
|
||||
{t("createMixinAccount")}
|
||||
</div>
|
||||
<Separator orientation="vertical" />
|
||||
<div
|
||||
onClick={handleScanToLogin}
|
||||
className="text-xs text-muted-foreground cursor-pointer"
|
||||
>
|
||||
{t("scanToLogin")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -160,7 +251,7 @@ export const MixinLoginForm = () => {
|
||||
<Button
|
||||
variant="default"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
className="w-full px-2"
|
||||
disabled={!code || code.length < 5 || !mixinId}
|
||||
onClick={() => {
|
||||
webApi
|
||||
@@ -40,7 +40,6 @@ export const MediaLoadingModal = () => {
|
||||
};
|
||||
|
||||
const LoadingContent = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
media,
|
||||
decoded,
|
||||
@@ -74,7 +73,7 @@ const LoadingContent = () => {
|
||||
isolate: data.isolate,
|
||||
});
|
||||
}}
|
||||
onCancel={() => navigate(-1)}
|
||||
onCancel={onCancel}
|
||||
transcribing={transcribing}
|
||||
transcribingProgress={transcribingProgress}
|
||||
transcribingOutput={transcribingOutput}
|
||||
@@ -109,12 +108,7 @@ const LoadingContent = () => {
|
||||
</div>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
onCancel ? onCancel() : navigate(-1);
|
||||
}}
|
||||
>
|
||||
<Button variant="secondary" onClick={onCancel}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
@@ -137,12 +131,7 @@ const LoadingContent = () => {
|
||||
)}
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
onCancel ? onCancel() : navigate(-1);
|
||||
}}
|
||||
>
|
||||
<Button variant="secondary" onClick={onCancel}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
export * from "./bandu-login-form";
|
||||
export * from "./db-state";
|
||||
export * from "./email-login-form";
|
||||
export * from "./layout";
|
||||
export * from "./loader-spin";
|
||||
export * from "./login-form";
|
||||
export * from "./github-login-form";
|
||||
export * from "./markdown-wrapper";
|
||||
export * from "./mixin-login-form";
|
||||
export * from "./no-records-found";
|
||||
export * from "./page-placeholder";
|
||||
export * from "./universal-player";
|
||||
|
||||
@@ -15,6 +15,9 @@ import {
|
||||
DropdownMenuItem,
|
||||
Separator,
|
||||
DialogTitle,
|
||||
Avatar,
|
||||
AvatarImage,
|
||||
DropdownMenuSeparator,
|
||||
} from "@renderer/components/ui";
|
||||
import {
|
||||
SettingsIcon,
|
||||
@@ -35,14 +38,14 @@ import {
|
||||
MessagesSquareIcon,
|
||||
PanelLeftOpenIcon,
|
||||
PanelLeftCloseIcon,
|
||||
ChevronsUpDownIcon,
|
||||
LogOutIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react";
|
||||
import { useLocation, Link } from "react-router-dom";
|
||||
import { useLocation, Link, useNavigate } from "react-router-dom";
|
||||
import { t } from "i18next";
|
||||
import { Preferences } from "@renderer/components";
|
||||
import {
|
||||
AppSettingsProviderContext,
|
||||
CopilotProviderContext,
|
||||
} from "@renderer/context";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { NoticiationsChannel } from "@renderer/cables";
|
||||
import { useState } from "react";
|
||||
@@ -51,7 +54,6 @@ export const Sidebar = () => {
|
||||
const location = useLocation();
|
||||
const activeTab = location.pathname;
|
||||
const { EnjoyApp, cable } = useContext(AppSettingsProviderContext);
|
||||
const { active, setActive } = useContext(CopilotProviderContext);
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -82,30 +84,17 @@ export const Sidebar = () => {
|
||||
return (
|
||||
<div
|
||||
className={`h-[100vh] transition-all relative ${
|
||||
isOpen ? "w-36" : "w-14"
|
||||
isOpen ? "w-48" : "w-12"
|
||||
}`}
|
||||
data-testid="sidebar"
|
||||
>
|
||||
<div
|
||||
className={`fixed top-0 left-0 h-full bg-muted ${
|
||||
isOpen ? "w-36" : "w-14"
|
||||
isOpen ? "w-48" : "w-12"
|
||||
}`}
|
||||
>
|
||||
<ScrollArea className="w-full h-full pb-12">
|
||||
<div className="py-4 mb-4 flex items-center space-x-1 justify-center">
|
||||
<img
|
||||
src="./assets/logo-light.svg"
|
||||
className="w-8 h-8 cursor-pointer hover:animate-spin"
|
||||
onClick={() => setActive(!active)}
|
||||
/>
|
||||
<span
|
||||
className={`text-xl font-semibold text-[#4797F5] ${
|
||||
isOpen ? "" : "hidden"
|
||||
}`}
|
||||
>
|
||||
ENJOY
|
||||
</span>
|
||||
</div>
|
||||
<SidebarHeader isOpen={isOpen} />
|
||||
<div className="grid gap-2 mb-4">
|
||||
<SidebarItem
|
||||
href="/"
|
||||
@@ -205,26 +194,6 @@ export const Sidebar = () => {
|
||||
|
||||
<Separator />
|
||||
|
||||
<SidebarItem
|
||||
href="/community"
|
||||
label={t("sidebar.community")}
|
||||
tooltip={t("sidebar.community")}
|
||||
active={activeTab === "/community"}
|
||||
Icon={UsersRoundIcon}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
|
||||
<SidebarItem
|
||||
href="/profile"
|
||||
label={t("sidebar.profile")}
|
||||
tooltip={t("sidebar.profile")}
|
||||
active={activeTab.startsWith("/profile")}
|
||||
Icon={UserIcon}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<div className="px-1">
|
||||
@@ -239,7 +208,7 @@ export const Sidebar = () => {
|
||||
data-tooltip-content={t("sidebar.preferences")}
|
||||
data-tooltip-place="right"
|
||||
>
|
||||
<SettingsIcon className="h-5 w-5" />
|
||||
<SettingsIcon className="size-4" />
|
||||
{isOpen && (
|
||||
<span className="ml-2"> {t("sidebar.preferences")} </span>
|
||||
)}
|
||||
@@ -268,24 +237,33 @@ export const Sidebar = () => {
|
||||
isOpen ? "justify-start" : "justify-center"
|
||||
}`}
|
||||
>
|
||||
<HelpCircleIcon className="h-5 w-5" />
|
||||
<HelpCircleIcon className="size-4" />
|
||||
{isOpen && (
|
||||
<span className="ml-2"> {t("sidebar.help")} </span>
|
||||
<>
|
||||
<span className="ml-2"> {t("sidebar.help")} </span>
|
||||
<ChevronRightIcon className="w-4 h-4 ml-auto" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent className="px-6">
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
EnjoyApp.shell.openExternal("https://998h.org/enjoy-app/")
|
||||
EnjoyApp.shell.openExternal(
|
||||
"https://1000h.org/enjoy-app/"
|
||||
)
|
||||
}
|
||||
className="flex justify-between space-x-4"
|
||||
>
|
||||
<span>{t("userGuide")}</span>
|
||||
<ExternalLinkIcon className="h-6 w-4" />
|
||||
<span className="min-w-fit">{t("userGuide")}</span>
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
@@ -305,7 +283,7 @@ export const Sidebar = () => {
|
||||
className="flex justify-between space-x-4"
|
||||
>
|
||||
<span>Mixin</span>
|
||||
<ExternalLinkIcon className="h-6 w-4" />
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
@@ -316,7 +294,7 @@ export const Sidebar = () => {
|
||||
className="flex justify-between space-x-4"
|
||||
>
|
||||
<span>Github</span>
|
||||
<ExternalLinkIcon className="h-6 w-4" />
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
@@ -335,9 +313,9 @@ export const Sidebar = () => {
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{isOpen ? (
|
||||
<PanelLeftCloseIcon className="h-5 w-5" />
|
||||
<PanelLeftCloseIcon className="size-4" />
|
||||
) : (
|
||||
<PanelLeftOpenIcon className="h-5 w-5" />
|
||||
<PanelLeftOpenIcon className="size-4" />
|
||||
)}
|
||||
{isOpen && <span className="ml-2"> {t("sidebar.collapse")} </span>}
|
||||
</Button>
|
||||
@@ -372,9 +350,72 @@ const SidebarItem = (props: {
|
||||
variant={active ? "default" : "ghost"}
|
||||
className={`w-full ${isOpen ? "justify-start" : "justify-center"}`}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
<Icon className="size-4" />
|
||||
{isOpen && <span className="ml-2">{label}</span>}
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarHeader = (props: { isOpen: boolean }) => {
|
||||
const { isOpen } = props;
|
||||
const { user, logout } = useContext(AppSettingsProviderContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="py-3 px-1 sticky top-0 bg-muted z-10">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={`w-full h-12 hover:bg-background ${
|
||||
isOpen ? "justify-start" : "justify-center px-1"
|
||||
}`}
|
||||
>
|
||||
<Avatar className="size-8">
|
||||
<AvatarImage src={user.avatarUrl} />
|
||||
</Avatar>
|
||||
{isOpen && (
|
||||
<>
|
||||
<div className="ml-2 flex flex-col leading-none">
|
||||
<span className="text-left text-sm font-medium line-clamp-1">
|
||||
{user.name}
|
||||
</span>
|
||||
<span className="text-left text-xs text-muted-foreground line-clamp-1">
|
||||
{user.id}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDownIcon className="size-4 ml-auto" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => navigate("/profile")}
|
||||
>
|
||||
<span>{t("sidebar.profile")}</span>
|
||||
<UserIcon className="size-4 ml-auto" />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => navigate("/community")}
|
||||
>
|
||||
<span>{t("sidebar.community")}</span>
|
||||
<UsersRoundIcon className="size-4 ml-auto" />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={logout} className="cursor-pointer">
|
||||
<span>{t("logout")}</span>
|
||||
<LogOutIcon className="size-4 ml-auto" />
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -131,7 +131,9 @@ export const BalanceSettings = () => {
|
||||
{[2, 10, 50, 100].map((amount) => (
|
||||
<div
|
||||
className={`text-xl w-full h-20 border rounded-md flex items-center justify-center cursor-pointer shadow hover:bg-gray-100 hover:dark:text-primary-foreground transition-colors duration-200 ease-in-out ${
|
||||
amount == depositAmount ? "bg-gray-100 dark:text-primary-foreground" : ""
|
||||
amount == depositAmount
|
||||
? "bg-gray-100 dark:text-primary-foreground"
|
||||
: ""
|
||||
}`}
|
||||
key={`deposit-amount-${amount}`}
|
||||
onClick={() => setDepositAmount(amount)}
|
||||
@@ -269,7 +271,7 @@ export const BalanceSettings = () => {
|
||||
};
|
||||
|
||||
const UsageChart = () => {
|
||||
const { webApi, EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const { webApi } = useContext(AppSettingsProviderContext);
|
||||
const [usages, setUsages] = useState<{ label: string; data: number[] }[]>([]);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
|
||||
@@ -115,7 +115,12 @@ export const EchogardenSttSettings = (props: {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="whisper">Whisper</SelectItem>
|
||||
<SelectItem value="whisper.cpp">Whisper.cpp</SelectItem>
|
||||
<SelectItem
|
||||
disabled={platformInfo?.platform === "darwin"}
|
||||
value="whisper.cpp"
|
||||
>
|
||||
Whisper.cpp
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
Avatar,
|
||||
AvatarImage,
|
||||
AvatarFallback,
|
||||
@@ -103,38 +94,14 @@ export const UserSettings = () => {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="text-destructive"
|
||||
size="sm"
|
||||
>
|
||||
{t("logout")}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("logout")}</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
{t("logoutConfirmation")}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive hover:bg-destructive-hover"
|
||||
onClick={() => {
|
||||
logout();
|
||||
redirect("/landing");
|
||||
}}
|
||||
>
|
||||
{t("logout")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="text-destructive"
|
||||
size="sm"
|
||||
onClick={logout}
|
||||
>
|
||||
{t("logout")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -45,7 +45,7 @@ export const RecordingCalendar = (props: {
|
||||
value: currentYear,
|
||||
},
|
||||
];
|
||||
const startYear = dayjs(user.createdAt).year();
|
||||
const startYear = dayjs(user?.createdAt).year();
|
||||
|
||||
for (let i = currentYear - 1; i >= startYear; i--) {
|
||||
_tabs.push({
|
||||
@@ -116,7 +116,7 @@ export const RecordingCalendar = (props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto p-4 rounded-lg border flex justify-between gap-4">
|
||||
<div className="mx-auto p-4 rounded-lg border flex justify-between gap-2">
|
||||
<Calendar
|
||||
data={stats}
|
||||
labels={{
|
||||
@@ -162,11 +162,12 @@ export const RecordingCalendar = (props: {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ScrollArea className="w-32 h-36 px-2">
|
||||
<ScrollArea className="max-w-fit h-36">
|
||||
{tabs().map((_tab) => (
|
||||
<Button
|
||||
key={_tab.value}
|
||||
className="w-full justify-start py-1 min-w-max text-xs xl:text-sm 2xl:text-md"
|
||||
size="sm"
|
||||
className="w-full justify-center py-1 px-1 min-w-max text-xs xl:text-sm 2xl:text-md mb-2"
|
||||
variant={_tab.value === tab ? "secondary" : "ghost"}
|
||||
onClick={() => handleTabChange(_tab.value)}
|
||||
>
|
||||
|
||||
@@ -38,3 +38,4 @@ export * from "./tabs";
|
||||
export * from "./textarea";
|
||||
export * from "./toggle";
|
||||
export * from "./tooltip";
|
||||
export * from "./input-otp";
|
||||
|
||||
70
enjoy/src/renderer/components/ui/input-otp.tsx
Normal file
70
enjoy/src/renderer/components/ui/input-otp.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { Minus } from "lucide-react";
|
||||
import { cn } from "@renderer/lib/utils";
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
||||
containerClassName
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
InputOTP.displayName = "InputOTP";
|
||||
|
||||
const InputOTPGroup = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
||||
));
|
||||
InputOTPGroup.displayName = "InputOTPGroup";
|
||||
|
||||
const InputOTPSlot = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive && "z-10 ring-1 ring-ring",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
InputOTPSlot.displayName = "InputOTPSlot";
|
||||
|
||||
const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Minus />
|
||||
</div>
|
||||
));
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator";
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||
@@ -8,8 +8,9 @@ export const VideoPlayer = (props: {
|
||||
id?: string;
|
||||
md5?: string;
|
||||
segmentIndex?: number;
|
||||
onLoad?: (video: VideoType) => void;
|
||||
}) => {
|
||||
const { id, md5, segmentIndex } = props;
|
||||
const { id, md5, segmentIndex, onLoad } = props;
|
||||
const { media, setMedia, setCurrentSegmentIndex, getCachedSegmentIndex } =
|
||||
useContext(MediaShadowProviderContext);
|
||||
const { video } = useVideo({ id, md5 });
|
||||
@@ -21,6 +22,7 @@ export const VideoPlayer = (props: {
|
||||
|
||||
useEffect(() => {
|
||||
setMedia(video);
|
||||
onLoad?.(video);
|
||||
}, [video]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -6,6 +6,20 @@ import ahoy from "ahoy.js";
|
||||
import { type Consumer, createConsumer } from "@rails/actioncable";
|
||||
import { DbProviderContext } from "@renderer/context";
|
||||
import { UserSettingKeyEnum } from "@/types/enums";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogCancel,
|
||||
AlertDialogAction,
|
||||
Button,
|
||||
} from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
import { redirect } from "react-router-dom";
|
||||
|
||||
type AppSettingsProviderState = {
|
||||
webApi: Client;
|
||||
@@ -72,6 +86,8 @@ export const AppSettingsProvider = ({
|
||||
const [ipaMappings, setIpaMappings] = useState<{ [key: string]: string }>(
|
||||
IPA_MAPPINGS
|
||||
);
|
||||
const [loggingOut, setLoggingOut] = useState<boolean>(false);
|
||||
|
||||
const db = useContext(DbProviderContext);
|
||||
|
||||
const fetchLanguages = async () => {
|
||||
@@ -321,7 +337,7 @@ export const AppSettingsProvider = ({
|
||||
setApiUrl: setApiUrlHandler,
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
logout: () => setLoggingOut(true),
|
||||
libraryPath,
|
||||
setLibraryPath: setLibraryPathHandler,
|
||||
proxy,
|
||||
@@ -337,6 +353,29 @@ export const AppSettingsProvider = ({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
<AlertDialog open={loggingOut} onOpenChange={setLoggingOut}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("logout")}</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
{t("logoutConfirmation")}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive hover:bg-destructive-hover"
|
||||
onClick={() => {
|
||||
logout();
|
||||
redirect("/landing");
|
||||
}}
|
||||
>
|
||||
{t("logout")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</AppSettingsProviderContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -447,7 +447,7 @@ export const ChatSessionProvider = ({
|
||||
updateMessage,
|
||||
}}
|
||||
>
|
||||
<MediaShadowProvider>
|
||||
<MediaShadowProvider onCancel={() => setShadowing(null)}>
|
||||
{chat && children}
|
||||
|
||||
<AlertDialog
|
||||
@@ -472,6 +472,7 @@ export const ChatSessionProvider = ({
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<Sheet
|
||||
modal={false}
|
||||
open={Boolean(shadowing)}
|
||||
|
||||
@@ -61,7 +61,7 @@ export const CourseProvider = ({
|
||||
setShadowing,
|
||||
}}
|
||||
>
|
||||
<MediaShadowProvider>
|
||||
<MediaShadowProvider onCancel={() => setShadowing(null)}>
|
||||
{children}
|
||||
<Sheet
|
||||
modal={false}
|
||||
|
||||
@@ -18,13 +18,14 @@ import { Tooltip } from "react-tooltip";
|
||||
import { useAudioRecorder } from "react-audio-voice-recorder";
|
||||
import { t } from "i18next";
|
||||
import { SttEngineOptionEnum } from "@/types/enums";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const ONE_MINUTE = 60;
|
||||
const TEN_MINUTES = 10 * ONE_MINUTE;
|
||||
|
||||
type MediaShadowContextType = {
|
||||
layout: "compact" | "normal";
|
||||
onCancel?: () => void;
|
||||
onCancel: () => void;
|
||||
media: AudioType | VideoType;
|
||||
setMedia: (media: AudioType | VideoType) => void;
|
||||
setMediaProvider: (mediaProvider: HTMLAudioElement | null) => void;
|
||||
@@ -116,6 +117,7 @@ export const MediaShadowProvider = ({
|
||||
const { EnjoyApp, learningLanguage, recorderConfig } = useContext(
|
||||
AppSettingsProviderContext
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [media, setMedia] = useState<AudioType | VideoType>(null);
|
||||
const [mediaProvider, setMediaProvider] = useState<HTMLAudioElement | null>(
|
||||
@@ -643,7 +645,7 @@ export const MediaShadowProvider = ({
|
||||
<MediaShadowProviderContext.Provider
|
||||
value={{
|
||||
layout,
|
||||
onCancel,
|
||||
onCancel: onCancel || (() => navigate(-1)),
|
||||
media,
|
||||
setMedia,
|
||||
setMediaProvider,
|
||||
|
||||
@@ -188,7 +188,9 @@ export const useTranscribe = () => {
|
||||
return {
|
||||
engine: "upload",
|
||||
model: "-",
|
||||
transcript: segmentTimeline.map((entry) => entry.text).join(" "),
|
||||
transcript: segmentTimeline
|
||||
.map((entry: TimelineEntry) => entry.text)
|
||||
.join(" "),
|
||||
segmentTimeline,
|
||||
};
|
||||
} else {
|
||||
|
||||
@@ -12,9 +12,7 @@ import { SttEngineOptionEnum } from "@/types/enums";
|
||||
import { t } from "i18next";
|
||||
|
||||
export const useTranscriptions = (media: AudioType | VideoType) => {
|
||||
const { sttEngine, echogardenSttConfig } = useContext(
|
||||
AISettingsProviderContext
|
||||
);
|
||||
const { sttEngine } = useContext(AISettingsProviderContext);
|
||||
const { EnjoyApp, learningLanguage, webApi } = useContext(
|
||||
AppSettingsProviderContext
|
||||
);
|
||||
|
||||
@@ -1,32 +1,56 @@
|
||||
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
useParams,
|
||||
useSearchParams,
|
||||
Link,
|
||||
useNavigate,
|
||||
} from "react-router-dom";
|
||||
import { AudioPlayer } from "@renderer/components";
|
||||
import { Button } from "@renderer/components/ui";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
import { MediaShadowProvider } from "@renderer/context";
|
||||
import { useState } from "react";
|
||||
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const segmentIndex = searchParams.get("segmentIndex") || "0";
|
||||
const [audio, setAudio] = useState<AudioType | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-screen flex flex-col relative">
|
||||
<div className="flex space-x-1 items-center h-12 px-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span className="text-sm">{t("shadowingAudio")}</span>
|
||||
</div>
|
||||
<div className="h-screen flex flex-col relative">
|
||||
<Breadcrumb className="px-4 pt-3 pb-2">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to={`/audios`}>{t("sidebar.audios")}</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>
|
||||
{audio?.name || t("shadowingAudio")}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
|
||||
<div className="flex-1">
|
||||
<MediaShadowProvider>
|
||||
<AudioPlayer id={id} segmentIndex={parseInt(segmentIndex)} />
|
||||
</MediaShadowProvider>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<MediaShadowProvider onCancel={() => navigate("/audios")}>
|
||||
<AudioPlayer
|
||||
id={id}
|
||||
segmentIndex={parseInt(segmentIndex)}
|
||||
onLoad={setAudio}
|
||||
/>
|
||||
</MediaShadowProvider>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import { AudiosComponent } from "@renderer/components";
|
||||
import { Button } from "@renderer/components/ui";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { t } from "i18next";
|
||||
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="h-full max-w-5xl mx-auto px-4 py-6">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.audios")}</span>
|
||||
</div>
|
||||
<div className="min-h-full max-w-5xl mx-auto px-4 py-6">
|
||||
<AudiosComponent />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Button } from "@renderer/components/ui";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { t } from "i18next";
|
||||
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="h-full max-w-5xl mx-auto px-4 py-6">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.books")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -14,30 +14,21 @@ export default () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="h-full px-4 lg:px-8 py-6">
|
||||
<div className="max-w-screen-md mx-auto mb-6">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.community")}</span>
|
||||
</div>
|
||||
<div className="min-h-full px-4 lg:px-8 py-6 max-w-screen-md mx-auto">
|
||||
<Tabs defaultValue="square">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="square">{t("square")}</TabsTrigger>
|
||||
<TabsTrigger value="rankings">{t("rankings")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<Tabs defaultValue="square">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="square">{t("square")}</TabsTrigger>
|
||||
<TabsTrigger value="rankings">{t("rankings")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="square">
|
||||
<Posts />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="square">
|
||||
<Posts />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="rankings">
|
||||
<UsersRankings />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<TabsContent value="rankings">
|
||||
<UsersRankings />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -213,129 +213,120 @@ export default () => {
|
||||
}, [currentGptEngine]);
|
||||
|
||||
return (
|
||||
<div className="h-full px-4 py-6 lg:px-8 flex flex-col">
|
||||
<div className="w-full max-w-screen-md mx-auto flex-1">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.aiAssistant")}</span>
|
||||
</div>
|
||||
<div className="min-h-full px-4 py-6 lg:px-8 max-w-5xl mx-auto">
|
||||
<div className="mb-6 flex justify-center">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
data-testid="conversation-new-button"
|
||||
className="h-12 rounded-lg w-96"
|
||||
>
|
||||
{t("newConversation")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<div className="my-6 flex justify-center">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
data-testid="conversation-new-button"
|
||||
className="h-12 rounded-lg w-96"
|
||||
>
|
||||
{t("newConversation")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("selectAiRole")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("selectAiRole")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div data-testid="conversation-presets" className="">
|
||||
<div className="text-sm text-foreground/70 mb-2">
|
||||
{t("chooseFromPresetGpts")}
|
||||
</div>
|
||||
<ScrollArea className="h-64 pr-4">
|
||||
{config.gptPresets.map((preset: any) => (
|
||||
<DialogTrigger
|
||||
key={preset.key}
|
||||
data-testid={`conversation-preset-${preset.key}`}
|
||||
asChild
|
||||
onClick={() => {
|
||||
setPreset(preset);
|
||||
setCreating(true);
|
||||
}}
|
||||
>
|
||||
<div className="w-full p-2 cursor-pointer rounded hover:bg-muted">
|
||||
<div className="capitalize truncate">{preset.name}</div>
|
||||
{preset.configuration.roleDefinition && (
|
||||
<div className="line-clamp-1 text-xs text-foreground/70">
|
||||
{preset.configuration.roleDefinition}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
))}
|
||||
</ScrollArea>
|
||||
<div data-testid="conversation-presets" className="">
|
||||
<div className="text-sm text-foreground/70 mb-2">
|
||||
{t("chooseFromPresetGpts")}
|
||||
</div>
|
||||
<ScrollArea className="h-64 pr-4">
|
||||
{config.gptPresets.map((preset: any) => (
|
||||
<DialogTrigger
|
||||
key={preset.key}
|
||||
data-testid={`conversation-preset-${preset.key}`}
|
||||
asChild
|
||||
onClick={() => {
|
||||
setPreset(preset);
|
||||
setCreating(true);
|
||||
}}
|
||||
>
|
||||
<div className="w-full p-2 cursor-pointer rounded hover:bg-muted">
|
||||
<div className="capitalize truncate">{preset.name}</div>
|
||||
{preset.configuration.roleDefinition && (
|
||||
<div className="line-clamp-1 text-xs text-foreground/70">
|
||||
{preset.configuration.roleDefinition}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
data-testid={`conversation-preset-${config.customPreset.key}`}
|
||||
onClick={() => {
|
||||
setPreset(config.customPreset);
|
||||
setCreating(true);
|
||||
}}
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
>
|
||||
{t("custom")} GPT
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
{config.ttsPreset.key && (
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
data-testid={`conversation-preset-${config.customPreset.key}`}
|
||||
data-testid={`conversation-preset-${config.ttsPreset.key}`}
|
||||
onClick={() => {
|
||||
setPreset(config.customPreset);
|
||||
setPreset(config.ttsPreset);
|
||||
setCreating(true);
|
||||
}}
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
>
|
||||
{t("custom")} GPT
|
||||
TTS
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
{config.ttsPreset.key && (
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
data-testid={`conversation-preset-${config.ttsPreset.key}`}
|
||||
onClick={() => {
|
||||
setPreset(config.ttsPreset);
|
||||
setCreating(true);
|
||||
}}
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
>
|
||||
TTS
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Sheet open={creating} onOpenChange={(value) => setCreating(value)}>
|
||||
<SheetContent className="p-0" aria-describedby={undefined}>
|
||||
<SheetHeader>
|
||||
<SheetTitle className="sr-only">
|
||||
{t("startConversation")}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="h-screen relative">
|
||||
<ConversationForm
|
||||
conversation={preset}
|
||||
onFinish={() => setCreating(false)}
|
||||
/>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
{conversations.map((conversation) => (
|
||||
<Link key={conversation.id} to={`/conversations/${conversation.id}`}>
|
||||
<ConversationCard conversation={conversation} />
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{hasMore && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => fetchConversations()}
|
||||
disabled={loading || !hasMore}
|
||||
className="px-4 py-2"
|
||||
>
|
||||
{t("loadMore")}
|
||||
{loading && <LoaderIcon className="w-4 h-4 animate-spin ml-2" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Sheet open={creating} onOpenChange={(value) => setCreating(value)}>
|
||||
<SheetContent className="p-0" aria-describedby={undefined}>
|
||||
<SheetHeader>
|
||||
<SheetTitle className="sr-only">
|
||||
{t("startConversation")}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="h-screen relative">
|
||||
<ConversationForm
|
||||
conversation={preset}
|
||||
onFinish={() => setCreating(false)}
|
||||
/>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
{conversations.map((conversation) => (
|
||||
<Link key={conversation.id} to={`/conversations/${conversation.id}`}>
|
||||
<ConversationCard conversation={conversation} />
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{hasMore && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => fetchConversations()}
|
||||
disabled={loading || !hasMore}
|
||||
className="px-4 py-2"
|
||||
>
|
||||
{t("loadMore")}
|
||||
{loading && <LoaderIcon className="w-4 h-4 animate-spin ml-2" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ export default () => {
|
||||
return (
|
||||
<CourseProvider id={id}>
|
||||
<div className="flex flex-col h-screen px-4 xl:px-6 py-6">
|
||||
<Breadcrumb className="mb-6">
|
||||
<Breadcrumb className="mb-4">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
@@ -51,9 +51,9 @@ export default () => {
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbPage>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to={`/courses/${id}`}>{chapter?.course?.title || id}</Link>
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbLink>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbPage>{chapter?.title || sequence}</BreadcrumbPage>
|
||||
</BreadcrumbList>
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { Button, toast } from "@renderer/components/ui";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
import { CourseCard } from "@renderer/components";
|
||||
import { CourseCard, LoaderSpin } from "@renderer/components";
|
||||
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="h-full max-w-5xl mx-auto px-4 py-6">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.courses")}</span>
|
||||
</div>
|
||||
<div className="">
|
||||
<CoursesList />
|
||||
</div>
|
||||
<div className="min-h-full max-w-5xl mx-auto px-4 py-6">
|
||||
<CoursesList />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -34,6 +22,7 @@ const CoursesList = () => {
|
||||
if (loading) return;
|
||||
if (!webApi) return;
|
||||
|
||||
setLoading(true);
|
||||
webApi
|
||||
.courses({ page: nextPage, language: learningLanguage })
|
||||
.then(({ courses = [], next }) => {
|
||||
@@ -48,6 +37,10 @@ const CoursesList = () => {
|
||||
fetchCourses();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <LoaderSpin />;
|
||||
}
|
||||
|
||||
if (!courses.length) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
|
||||
@@ -34,8 +34,8 @@ export default () => {
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div className="h-full max-w-5xl mx-auto px-4 py-6">
|
||||
<Breadcrumb className="mb-6">
|
||||
<div className="min-h-full max-w-5xl mx-auto px-4 py-6">
|
||||
<Breadcrumb className="mb-4">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
@@ -113,7 +113,9 @@ const CourseDetail = (props: { course: CourseType; onUpdate: () => void }) => {
|
||||
{course.enrollmentsCount > 0 && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<UsersIcon className="w-4 h-4" />
|
||||
<span className="text-sm text-muted-foreground">{course.enrollmentsCount}</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{course.enrollmentsCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import {
|
||||
Button,
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbSeparator,
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
@@ -13,9 +17,9 @@ import {
|
||||
DocumentTextRenderer,
|
||||
} from "@renderer/components";
|
||||
import { useContext } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { DocumentProvider, DocumentProviderContext } from "@renderer/context";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { t } from "i18next";
|
||||
|
||||
export default () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -29,7 +33,6 @@ export default () => {
|
||||
|
||||
const DocumentComponent = () => {
|
||||
const { document, playingSegment } = useContext(DocumentProviderContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!document) {
|
||||
return (
|
||||
@@ -41,12 +44,19 @@ const DocumentComponent = () => {
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col relative">
|
||||
<div className="flex space-x-1 items-center px-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span className="text-sm line-clamp-1">{document.title}</span>
|
||||
</div>
|
||||
<Breadcrumb className="px-4 pt-3 pb-2">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to={`/documents`}>{t("sidebar.documents")}</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<span className="text-sm line-clamp-1">{document.title}</span>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
|
||||
<ResizablePanelGroup direction="horizontal" className="p-4">
|
||||
<ResizablePanel id="document" order={0}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Input } from "@renderer/components/ui";
|
||||
import { Input } from "@renderer/components/ui";
|
||||
import {
|
||||
DocumentAddButton,
|
||||
DocumentCard,
|
||||
@@ -7,13 +7,10 @@ import {
|
||||
import { useState, useContext, useEffect } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { t } from "i18next";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useDebounce } from "@uidotdev/usehooks";
|
||||
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [documents, setDocuments] = useState<DocumentEType[]>([]);
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
@@ -37,13 +34,7 @@ export default () => {
|
||||
}, [debouncedQuery]);
|
||||
|
||||
return (
|
||||
<div className="h-full max-w-5xl mx-auto px-4 py-6">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.documents")}</span>
|
||||
</div>
|
||||
<div className="min-h-full max-w-5xl mx-auto px-4 py-6">
|
||||
<div className="flex flex-wrap items-center gap-4 mb-4">
|
||||
<Input
|
||||
className="max-w-48"
|
||||
|
||||
@@ -48,41 +48,34 @@ export default function Notes() {
|
||||
|
||||
return (
|
||||
<div className="min-h-[100vh] w-full max-w-5xl mx-auto px-4 py-6 lg:px-8">
|
||||
<div className="w-full max-w-screen-md mx-auto">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.notes")}</span>
|
||||
</div>
|
||||
|
||||
{groups.length === 0 && (
|
||||
<div className="flex justify-center">
|
||||
<div className="my-4 text-muted-foreground text-sm">{t("noNotesYet")}</div>
|
||||
{groups.length === 0 && (
|
||||
<div className="flex justify-center">
|
||||
<div className="my-4 text-muted-foreground text-sm">
|
||||
{t("noNotesYet")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col space-y-4">
|
||||
{groups.map((group) => (
|
||||
<NoteSegmentGroup
|
||||
key={group.targetId}
|
||||
count={group.count}
|
||||
segment={group.segment}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasMore && (
|
||||
<div className="flex justify-center mt-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => findNotesGroup({ offset: groups.length })}
|
||||
>
|
||||
{t("loadMore")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col space-y-4">
|
||||
{groups.map((group) => (
|
||||
<NoteSegmentGroup
|
||||
key={group.targetId}
|
||||
count={group.count}
|
||||
segment={group.segment}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{hasMore && (
|
||||
<div className="flex justify-center mt-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => findNotesGroup({ offset: groups.length })}
|
||||
>
|
||||
{t("loadMore")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,31 +25,20 @@ export default () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full px-4 py-6 lg:px-8">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.profile")}</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<RecordingStats />
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<RecordingCalendar
|
||||
onSelectRange={(from, to) => {
|
||||
setRange([from, to]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<RecordingActivities from={range[0]} to={range[1]} />
|
||||
</div>
|
||||
<div className="min-h-full px-4 py-6 lg:px-8 max-w-5xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<RecordingStats />
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<RecordingCalendar
|
||||
onSelectRange={(from, to) => {
|
||||
setRange([from, to]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RecordingActivities from={range[0]} to={range[1]} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -138,15 +138,8 @@ export default () => {
|
||||
}, [orderBy]);
|
||||
|
||||
return (
|
||||
<div className="h-full px-4 py-6 lg:px-8">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.pronunciationAssessment")}</span>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<div className="min-h-full px-4 py-6 lg:px-8 max-w-5xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -269,6 +262,6 @@ export default () => {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { t } from "i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Button } from "@renderer/components/ui";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@renderer/components/ui";
|
||||
import { PronunciationAssessmentForm } from "@renderer/components";
|
||||
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="h-full px-4 py-6 lg:px-8">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<Link to="/pronunciation_assessments">
|
||||
{t("sidebar.pronunciationAssessment")}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span>{t("newAssessment")}</span>
|
||||
</div>
|
||||
<div className="">
|
||||
<PronunciationAssessmentForm />
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-full px-4 py-6 lg:px-8 max-w-5xl mx-auto">
|
||||
<Breadcrumb className="mb-4">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to={`/pronunciation_assessments`}>
|
||||
{t("sidebar.pronunciationAssessment")}
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{t("newAssessment")}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<PronunciationAssessmentForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,9 +2,23 @@ import { useContext, useEffect, useState } from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { AppSettingsProviderContext } from "../context";
|
||||
import { LoaderSpin, Posts } from "@renderer/components";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { t } from "i18next";
|
||||
import { Avatar, AvatarFallback, AvatarImage, Button, Tabs, TabsContent, TabsList, TabsTrigger } from "@renderer/components/ui";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
Button,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@renderer/components/ui";
|
||||
|
||||
export default () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -18,19 +32,19 @@ export default () => {
|
||||
webApi.user(id).then((user) => {
|
||||
setUser(user);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const follow = () => {
|
||||
webApi.follow(id).then(() => {
|
||||
setUser({ ...user, following: true });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const unfollow = () => {
|
||||
webApi.unfollow(id).then(() => {
|
||||
setUser({ ...user, following: false });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUser();
|
||||
@@ -41,12 +55,17 @@ export default () => {
|
||||
return (
|
||||
<div className="h-full px-4 py-6 lg:px-8">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{user.name}</span>
|
||||
</div>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to="/community">{t("sidebar.community")}</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbPage>{user.name}</BreadcrumbPage>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-center mb-2">
|
||||
@@ -58,34 +77,55 @@ export default () => {
|
||||
</Avatar>
|
||||
</div>
|
||||
|
||||
{
|
||||
currentUser.id != user.id && <div className="flex justify-center">{
|
||||
user.following ? <Button variant="link" className="text-destructive" size="sm" onClick={unfollow}>{t('unfollow')}</Button> : <Button size="sm" onClick={follow}>{t('follow')}</Button>
|
||||
}</div>
|
||||
}
|
||||
{currentUser.id != user.id && (
|
||||
<div className="flex justify-center">
|
||||
{user.following ? (
|
||||
<Button
|
||||
variant="link"
|
||||
className="text-destructive"
|
||||
size="sm"
|
||||
onClick={unfollow}
|
||||
>
|
||||
{t("unfollow")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="sm" onClick={follow}>
|
||||
{t("follow")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-w-screen-sm mx-auto">
|
||||
<Tabs defaultValue="activities">
|
||||
<div className="w-full flex justify-center">
|
||||
<TabsList>
|
||||
<TabsTrigger value="activities">{t('activities')}</TabsTrigger>
|
||||
<TabsTrigger value="followers"><span className="capitalize">{t('followers')}</span></TabsTrigger>
|
||||
<TabsTrigger value="following"><span className="capitalize">{t('following')}</span></TabsTrigger>
|
||||
<TabsTrigger value="activities">{t("activities")}</TabsTrigger>
|
||||
<TabsTrigger value="followers">
|
||||
<span className="capitalize">{t("followers")}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="following">
|
||||
<span className="capitalize">{t("following")}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="activities">
|
||||
<Posts userId={user.id} />
|
||||
</TabsContent>
|
||||
<TabsContent value="followers"><UserFollowers id={user.id} /></TabsContent>
|
||||
<TabsContent value="following"><UserFollowing id={user.id} /></TabsContent>
|
||||
<TabsContent value="followers">
|
||||
<UserFollowers id={user.id} />
|
||||
</TabsContent>
|
||||
<TabsContent value="following">
|
||||
<UserFollowing id={user.id} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const UserFollowers = (props: { id: string }) => {
|
||||
const { id } = props;
|
||||
@@ -100,17 +140,22 @@ const UserFollowers = (props: { id: string }) => {
|
||||
setUsers(res.users);
|
||||
setPage(res.next);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchFollowers();
|
||||
return () => {
|
||||
setUsers([]);
|
||||
setPage(1);
|
||||
}
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
if (users.length === 0) return <div className="w-full px-4 py-6 text-center text-sm text-muted-foreground">{t("noFollowersYet")}</div>;
|
||||
if (users.length === 0)
|
||||
return (
|
||||
<div className="w-full px-4 py-6 text-center text-sm text-muted-foreground">
|
||||
{t("noFollowersYet")}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -119,14 +164,14 @@ const UserFollowers = (props: { id: string }) => {
|
||||
<UserCard key={user.id} user={user} />
|
||||
))}
|
||||
</div>
|
||||
{
|
||||
page && (<div className="flex justify-center py-4">
|
||||
{page && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={() => fetchFollowers()}>{t("loadMore")}</Button>
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const UserFollowing = (props: { id: string }) => {
|
||||
const { id } = props;
|
||||
@@ -141,17 +186,22 @@ const UserFollowing = (props: { id: string }) => {
|
||||
setUsers(res.users);
|
||||
setPage(res.next);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchFollowing();
|
||||
return () => {
|
||||
setUsers([]);
|
||||
setPage(1);
|
||||
}
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
if (users.length === 0) return <div className="w-full px-4 py-6 text-center text-sm text-muted-foreground">{t("notFollowingAnyoneYet")}</div>;
|
||||
if (users.length === 0)
|
||||
return (
|
||||
<div className="w-full px-4 py-6 text-center text-sm text-muted-foreground">
|
||||
{t("notFollowingAnyoneYet")}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -160,14 +210,14 @@ const UserFollowing = (props: { id: string }) => {
|
||||
<UserCard key={user.id} user={user} />
|
||||
))}
|
||||
</div>
|
||||
{
|
||||
page && (<div className="flex justify-center py-4">
|
||||
{page && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={() => fetchFollowing()}>{t("loadMore")}</Button>
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const UserCard = ({ user }: { user: UserType }) => {
|
||||
const { webApi, user: currentUser } = useContext(AppSettingsProviderContext);
|
||||
@@ -203,16 +253,16 @@ const UserCard = ({ user }: { user: UserType }) => {
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
{
|
||||
currentUser.id != user.id && <Button
|
||||
{currentUser.id != user.id && (
|
||||
<Button
|
||||
variant={following ? "secondary" : "default"}
|
||||
size="sm"
|
||||
onClick={handleFollow}
|
||||
>
|
||||
{following ? t("unfollow") : t("follow")}
|
||||
</Button>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,30 +1,56 @@
|
||||
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
useParams,
|
||||
useSearchParams,
|
||||
Link,
|
||||
useNavigate,
|
||||
} from "react-router-dom";
|
||||
import { VideoPlayer } from "@renderer/components";
|
||||
import { Button } from "@renderer/components/ui";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
import { MediaShadowProvider } from "@renderer/context";
|
||||
import { useState } from "react";
|
||||
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const segmentIndex = searchParams.get("segmentIndex") || "0";
|
||||
const [video, setVideo] = useState<VideoType | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-screen flex flex-col relative">
|
||||
<div className="flex space-x-1 items-center h-12 px-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("shadowingVideo")}</span>
|
||||
</div>
|
||||
<div className="h-screen flex flex-col relative">
|
||||
<Breadcrumb className="px-4 pt-3 pb-2">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to={`/videos`}>{t("sidebar.videos")}</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>
|
||||
{video?.name || t("shadowingVideo")}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
|
||||
<MediaShadowProvider>
|
||||
<VideoPlayer id={id} segmentIndex={parseInt(segmentIndex)} />
|
||||
<div className="flex-1">
|
||||
<MediaShadowProvider onCancel={() => navigate("/videos")}>
|
||||
<VideoPlayer
|
||||
id={id}
|
||||
segmentIndex={parseInt(segmentIndex)}
|
||||
onLoad={setVideo}
|
||||
/>
|
||||
</MediaShadowProvider>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import { VideosComponent } from "@renderer/components";
|
||||
import { Button } from "@renderer/components/ui";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { t } from "i18next";
|
||||
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="h-full max-w-5xl mx-auto px-4 py-6">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.videos")}</span>
|
||||
</div>
|
||||
<div className="min-h-full max-w-5xl mx-auto px-4 py-6">
|
||||
<VideosComponent />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { Button } from "@renderer/components/ui";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState, useContext, useEffect } from "react";
|
||||
import {
|
||||
AppSettingsProviderContext,
|
||||
HotKeysSettingsProviderContext,
|
||||
} from "@renderer/context";
|
||||
import { LoaderSpin, MeaningMemorizingCard } from "@renderer/components";
|
||||
import { t } from "i18next";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [meanings, setMeanings] = useState<MeaningType[]>([]);
|
||||
const { webApi } = useContext(AppSettingsProviderContext);
|
||||
@@ -24,7 +20,9 @@ export default () => {
|
||||
|
||||
const fetchMeanings = async (page: number = nextPage) => {
|
||||
if (!page) return;
|
||||
if (loading) return;
|
||||
|
||||
setLoading(true);
|
||||
webApi
|
||||
.mineMeanings({ page, items: 10 })
|
||||
.then((response) => {
|
||||
@@ -67,13 +65,6 @@ export default () => {
|
||||
return (
|
||||
<div className="h-[100vh]">
|
||||
<div className="max-w-screen-md mx-auto p-4">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.vocabulary")}</span>
|
||||
</div>
|
||||
|
||||
{meanings.length === 0 ? (
|
||||
<div className=""></div>
|
||||
) : (
|
||||
@@ -89,7 +80,7 @@ export default () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
<ChevronLeftIcon className="size-5" />
|
||||
</Button>
|
||||
<div className="bg-background flex-1 h-5/6 border p-6 rounded-xl shadow-xl">
|
||||
<MeaningMemorizingCard meaning={meanings[currentIndex]} />
|
||||
@@ -108,7 +99,7 @@ export default () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChevronRightIcon className="w-5 h-5" />
|
||||
<ChevronRightIcon className="size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,6 @@ import Stories from "./pages/stories";
|
||||
import Story from "./pages/story";
|
||||
import Documents from "./pages/documents";
|
||||
import Document from "./pages/document";
|
||||
import Books from "./pages/books";
|
||||
import Profile from "./pages/profile";
|
||||
import User from "./pages/user";
|
||||
import Home from "./pages/home";
|
||||
@@ -114,10 +113,6 @@ export default createHashRouter([
|
||||
path: "/stories/:id",
|
||||
element: <Story />,
|
||||
},
|
||||
{
|
||||
path: "/books",
|
||||
element: <Books />,
|
||||
},
|
||||
{
|
||||
path: "/stories/preview/:uri",
|
||||
element: <StoryPreview />,
|
||||
|
||||
338
yarn.lock
338
yarn.lock
@@ -17,7 +17,7 @@ __metadata:
|
||||
markdown-it-sub: "npm:^2.0.0"
|
||||
markdown-it-sup: "npm:^2.0.0"
|
||||
mermaid: "npm:^11.4.0"
|
||||
sass: "npm:^1.80.6"
|
||||
sass: "npm:^1.80.7"
|
||||
swiper: "npm:^11.1.14"
|
||||
vitepress: "npm:^1.5.0"
|
||||
vitepress-plugin-mermaid: "npm:^2.0.17"
|
||||
@@ -33,8 +33,8 @@ __metadata:
|
||||
autoprefixer: "npm:^10.4.20"
|
||||
nuxt: "npm:^3.14.159"
|
||||
nuxt-og-image: "npm:^3.0.8"
|
||||
postcss: "npm:^8.4.48"
|
||||
sass: "npm:^1.80.6"
|
||||
postcss: "npm:^8.4.49"
|
||||
sass: "npm:^1.80.7"
|
||||
tailwindcss: "npm:^3.4.14"
|
||||
vue: "npm:^3.5.12"
|
||||
vue-router: "npm:^4.4.5"
|
||||
@@ -1689,10 +1689,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@echogarden/espeak-ng-emscripten@npm:^0.3.0":
|
||||
version: 0.3.0
|
||||
resolution: "@echogarden/espeak-ng-emscripten@npm:0.3.0"
|
||||
checksum: 10c0/7163023b91394eda5ded0fd2e819a14944edd4888beee4c1b87f1095ec536802749636c8f69144391d2ebf171ed23c217a2aa44ac48959f71ee8391ccdc47880
|
||||
"@echogarden/espeak-ng-emscripten@npm:^0.3.2":
|
||||
version: 0.3.2
|
||||
resolution: "@echogarden/espeak-ng-emscripten@npm:0.3.2"
|
||||
checksum: 10c0/d6183d66c649c1d25ad93b5553145f053425918ea33335e2952d6c64064a7eca4335cfbbe7e58b71c8877c83341bf355645b93c12669bcc71fdd03b8fdf8fbd7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1717,13 +1717,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@echogarden/kissfft-wasm@npm:^0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "@echogarden/kissfft-wasm@npm:0.2.0"
|
||||
checksum: 10c0/f02a8c6101e50f7c01d50181295e8638bbb11d23e601b4dfda482cbac90f617a1116ea0177263429e3647b686052d0ab95ca56fb2a77e75ac76cccee9d4996a9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@echogarden/pffft-wasm@npm:^0.4.2":
|
||||
version: 0.4.2
|
||||
resolution: "@echogarden/pffft-wasm@npm:0.4.2"
|
||||
@@ -3020,9 +3013,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@langchain/community@npm:^0.3.12":
|
||||
version: 0.3.12
|
||||
resolution: "@langchain/community@npm:0.3.12"
|
||||
"@langchain/community@npm:^0.3.14":
|
||||
version: 0.3.14
|
||||
resolution: "@langchain/community@npm:0.3.14"
|
||||
dependencies:
|
||||
"@langchain/openai": "npm:>=0.2.0 <0.4.0"
|
||||
binary-extensions: "npm:^2.2.0"
|
||||
@@ -3399,13 +3392,13 @@ __metadata:
|
||||
optional: true
|
||||
youtubei.js:
|
||||
optional: true
|
||||
checksum: 10c0/a4301523063302fc0cfcc9e8f60526e734a928170e307d0d4346afd3d8875272097e105e30f9bf112c6577332441612017e5915e5e11ed837c8c9a853f1fee39
|
||||
checksum: 10c0/c70799154affa120804e96d0e6eed5be152e726b7f78590ccf31558bbd1ff1653a7e3c998d66410734d6f83fa2203eb017c5ee5be24c345400ca53c71bd53d7c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@langchain/core@npm:^0.3.17":
|
||||
version: 0.3.17
|
||||
resolution: "@langchain/core@npm:0.3.17"
|
||||
"@langchain/core@npm:^0.3.18":
|
||||
version: 0.3.18
|
||||
resolution: "@langchain/core@npm:0.3.18"
|
||||
dependencies:
|
||||
ansi-styles: "npm:^5.0.0"
|
||||
camelcase: "npm:6"
|
||||
@@ -3418,7 +3411,7 @@ __metadata:
|
||||
uuid: "npm:^10.0.0"
|
||||
zod: "npm:^3.22.4"
|
||||
zod-to-json-schema: "npm:^3.22.3"
|
||||
checksum: 10c0/981cc8d81c24d7d64f574f13aaaa2478f01ce763d6431d60c5d07954c403ca4cff5c802fee288f264a7442358355ea35ad5fd6a02bb7df6bbadcf1bfd21fc181
|
||||
checksum: 10c0/0f9733b44148269f840b48429ace6458e0ec10ef478133f252ff0ad86f286239437289d4276582c2f8f29c61d86ec8a5dd30a77ae89e030b0af85c99695602c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5218,9 +5211,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-scroll-area@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@radix-ui/react-scroll-area@npm:1.2.0"
|
||||
"@radix-ui/react-scroll-area@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "@radix-ui/react-scroll-area@npm:1.2.1"
|
||||
dependencies:
|
||||
"@radix-ui/number": "npm:1.1.0"
|
||||
"@radix-ui/primitive": "npm:1.1.0"
|
||||
@@ -5241,7 +5234,7 @@ __metadata:
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 10c0/e33eca1b9f254003a08b2647add9278b1d90dd2e9128cbdd73b310e9363c4461d7149fd973e81ddcbdbf420b040b8fe864684919c58956a5f6e84fff2d1340b9
|
||||
checksum: 10c0/cea8a82e52bc60d0a087ae57609c735862b473f193a3ce827b4077ff4998f29b912da10afb3a46466d753ba2202ece82c405fd9647b1a822748d48ce4902e521
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5449,9 +5442,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-tooltip@npm:^1.1.3":
|
||||
version: 1.1.3
|
||||
resolution: "@radix-ui/react-tooltip@npm:1.1.3"
|
||||
"@radix-ui/react-tooltip@npm:^1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "@radix-ui/react-tooltip@npm:1.1.4"
|
||||
dependencies:
|
||||
"@radix-ui/primitive": "npm:1.1.0"
|
||||
"@radix-ui/react-compose-refs": "npm:1.1.0"
|
||||
@@ -5475,7 +5468,7 @@ __metadata:
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 10c0/388f2b74277dc8ac39ef6218e61f5cbebdc9ff5d03a8759bbd9d234561f43fab2771c4537c2e0faaaa19976d5b4cf7eb08112a493dc119e8abc45cbe7a416c97
|
||||
checksum: 10c0/721cfb0bf34e74af5a58d89a73e087522517b9502fb7ae9d1dc99137d4952f2bfd1696001e17aa83dfb533c39b4333030149562ebfe62d31238a1a2995bc6bd6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7726,15 +7719,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:^8.13.0":
|
||||
version: 8.13.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.13.0"
|
||||
"@typescript-eslint/eslint-plugin@npm:^8.14.0":
|
||||
version: 8.14.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.14.0"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": "npm:^4.10.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.13.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.13.0"
|
||||
"@typescript-eslint/utils": "npm:8.13.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.13.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.14.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.14.0"
|
||||
"@typescript-eslint/utils": "npm:8.14.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.14.0"
|
||||
graphemer: "npm:^1.4.0"
|
||||
ignore: "npm:^5.3.1"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
@@ -7745,66 +7738,66 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10c0/ee96515e9def17b0d1b8d568d4afcd21c5a8a1bc01bf2f30c4d1f396b41a2f49de3508f79c6231a137ca06943dd6933ac00032652190ab99a4e935ffef44df0b
|
||||
checksum: 10c0/46c82eb45be82ffec0ab04728a5180691b1d17002c669864861a3044b6d2105a75ca23cc80d18721b40b5e7dff1eff4ed68a43d726e25d55f3e466a9fbeeb873
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:^8.13.0":
|
||||
version: 8.13.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.13.0"
|
||||
"@typescript-eslint/parser@npm:^8.14.0":
|
||||
version: 8.14.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.14.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": "npm:8.13.0"
|
||||
"@typescript-eslint/types": "npm:8.13.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.13.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.13.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.14.0"
|
||||
"@typescript-eslint/types": "npm:8.14.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.14.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.14.0"
|
||||
debug: "npm:^4.3.4"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10c0/fa04f6c417c0f72104e148f1d7ff53e04108d383550365a556fbfae5d2283484696235db522189e17bc49039946977078e324100cef991ca01f78704182624ad
|
||||
checksum: 10c0/522b7afd25cd302c0510cc71985ba55ff92ecc5dbe3fc74a76fefea0169252fdd4b8cad6291fef05f63dfc173951af450dca20859c7f23e387b2e7410e8b97b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.13.0":
|
||||
version: 8.13.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.13.0"
|
||||
"@typescript-eslint/scope-manager@npm:8.14.0":
|
||||
version: 8.14.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.14.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.13.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.13.0"
|
||||
checksum: 10c0/1924b3e740e244d98f8a99740b4196d23ae3263303b387c66db94e140455a3132e603a130f3f70fc71e37f4bda5d0c0c67224ae3911908b097ef3f972c136be4
|
||||
"@typescript-eslint/types": "npm:8.14.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.14.0"
|
||||
checksum: 10c0/1e1295c6f9febadf63559aad328b23d960510ce6b4c9f74e10d881c3858fa7f1db767cd1af5272d2fe7c9c5c7daebee71854e6f841e413e5d70af282f6616e26
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.13.0":
|
||||
version: 8.13.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.13.0"
|
||||
"@typescript-eslint/type-utils@npm:8.14.0":
|
||||
version: 8.14.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.14.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree": "npm:8.13.0"
|
||||
"@typescript-eslint/utils": "npm:8.13.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.14.0"
|
||||
"@typescript-eslint/utils": "npm:8.14.0"
|
||||
debug: "npm:^4.3.4"
|
||||
ts-api-utils: "npm:^1.3.0"
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10c0/65319084616f3aea3d9f8dfab30c9b0a70de7314b445805016fdf0d0e39fe073eef2813c3e16c3e1c6a40462ba8eecfdbb12ab1e8570c3407a1cccdb69d4bc8b
|
||||
checksum: 10c0/42616a664b38ca418e13504247e5e1bad6ae85c045b48e5735ffab977d4bd58cc86fb9d2292bbb314fa408d78d4b0454c3a27dbf9f881f9921917a942825c806
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.13.0":
|
||||
version: 8.13.0
|
||||
resolution: "@typescript-eslint/types@npm:8.13.0"
|
||||
checksum: 10c0/bd3f88b738a92b2222f388bcf831357ef8940a763c2c2eb1947767e1051dd2f8bee387020e8cf4c2309e4142353961b659abc2885e30679109a0488b0bfefc23
|
||||
"@typescript-eslint/types@npm:8.14.0":
|
||||
version: 8.14.0
|
||||
resolution: "@typescript-eslint/types@npm:8.14.0"
|
||||
checksum: 10c0/7707f900e24e60e6780c5705f69627b7c0ef912cb3b095dfc8f4a0c84e866c66b1c4c10278cf99724560dc66985ec640750c4192786a09b853f9bb4c3ca5a7ce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.13.0":
|
||||
version: 8.13.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.13.0"
|
||||
"@typescript-eslint/typescript-estree@npm:8.14.0":
|
||||
version: 8.14.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.14.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.13.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.13.0"
|
||||
"@typescript-eslint/types": "npm:8.14.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.14.0"
|
||||
debug: "npm:^4.3.4"
|
||||
fast-glob: "npm:^3.3.2"
|
||||
is-glob: "npm:^4.0.3"
|
||||
@@ -7814,31 +7807,31 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10c0/2d45bc5ed4ac352bea927167ac28ef23bd13b6ae352ff50e85cddfdc4b06518f1dd4ae5f2495e30d6f62d247987677a4e807065d55829ba28963908a821dc96d
|
||||
checksum: 10c0/5e890d22bd067095f871cf144907a8c302db5b5f014c58906ad58d7f23569951cba805042eac6844744e5abb0d3648c9cc221a91b0703da0a8d6345dc1f83e74
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.13.0":
|
||||
version: 8.13.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.13.0"
|
||||
"@typescript-eslint/utils@npm:8.14.0":
|
||||
version: 8.14.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.14.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.4.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.13.0"
|
||||
"@typescript-eslint/types": "npm:8.13.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.13.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.14.0"
|
||||
"@typescript-eslint/types": "npm:8.14.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.14.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
checksum: 10c0/3fc5a7184a949df5f5b64f6af039a1d21ef7fe15f3d88a5d485ccbb535746d18514751143993a5aee287228151be3e326baf8f899a0a0a93368f6f20857ffa6d
|
||||
checksum: 10c0/1fcc2651d870832a799a5d1c85fc9421853508a006d6a6073c8316b012489dda77e123d13aea8f53eb9030a2da2c0eb273a6946a9941caa2519b99b33e89b720
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.13.0":
|
||||
version: 8.13.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.13.0"
|
||||
"@typescript-eslint/visitor-keys@npm:8.14.0":
|
||||
version: 8.14.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.14.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.13.0"
|
||||
"@typescript-eslint/types": "npm:8.14.0"
|
||||
eslint-visitor-keys: "npm:^3.4.3"
|
||||
checksum: 10c0/50b35f3cf673aaed940613f0007f7c4558a89ebef15c49824e65b6f084b700fbf01b01a4e701e24bbe651297a39678645e739acd255255f1603867a84bef0383
|
||||
checksum: 10c0/d0faf70ed9ecff5e36694bbb161a90bea6db59e0e79a7d4f264d67d565c12b13733d664b736b2730935f013c87ce3155cea954a533d28e99987681bc5f6259c3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11592,18 +11585,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"echogarden@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "echogarden@npm:2.0.0"
|
||||
"echogarden@npm:2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "echogarden@npm:2.0.3"
|
||||
dependencies:
|
||||
"@aws-sdk/client-polly": "npm:^3.687.0"
|
||||
"@aws-sdk/client-transcribe-streaming": "npm:^3.687.0"
|
||||
"@echogarden/audio-io": "npm:^0.2.3"
|
||||
"@echogarden/espeak-ng-emscripten": "npm:^0.3.0"
|
||||
"@echogarden/espeak-ng-emscripten": "npm:^0.3.2"
|
||||
"@echogarden/fasttext-wasm": "npm:^0.1.0"
|
||||
"@echogarden/flite-wasi": "npm:^0.1.1"
|
||||
"@echogarden/fvad-wasm": "npm:^0.2.0"
|
||||
"@echogarden/kissfft-wasm": "npm:^0.2.0"
|
||||
"@echogarden/pffft-wasm": "npm:^0.4.2"
|
||||
"@echogarden/rnnoise-wasm": "npm:^0.2.0"
|
||||
"@echogarden/rubberband-wasm": "npm:^0.2.0"
|
||||
@@ -11620,7 +11612,6 @@ __metadata:
|
||||
fs-extra: "npm:^11.2.0"
|
||||
gaxios: "npm:^6.7.1"
|
||||
graceful-fs: "npm:^4.2.11"
|
||||
html-escaper: "npm:^3.0.3"
|
||||
html-to-text: "npm:^9.0.5"
|
||||
import-meta-resolve: "npm:^4.1.0"
|
||||
jieba-wasm: "npm:^2.2.0"
|
||||
@@ -11628,10 +11619,9 @@ __metadata:
|
||||
json5: "npm:^2.2.3"
|
||||
kuromoji: "npm:^0.1.2"
|
||||
microsoft-cognitiveservices-speech-sdk: "npm:^1.41.0"
|
||||
moving-median: "npm:^1.0.0"
|
||||
msgpack-lite: "npm:^0.1.26"
|
||||
onnxruntime-node: "npm:^1.20.0"
|
||||
openai: "npm:^4.71.1"
|
||||
openai: "npm:^4.72.0"
|
||||
sam-js: "npm:^0.3.1"
|
||||
strip-ansi: "npm:^7.1.0"
|
||||
tar: "npm:^7.4.3"
|
||||
@@ -11986,8 +11976,8 @@ __metadata:
|
||||
"@electron-forge/publisher-s3": "npm:^7.5.0"
|
||||
"@electron/fuses": "npm:^1.8.0"
|
||||
"@hookform/resolvers": "npm:^3.9.1"
|
||||
"@langchain/community": "npm:^0.3.12"
|
||||
"@langchain/core": "npm:^0.3.17"
|
||||
"@langchain/community": "npm:^0.3.14"
|
||||
"@langchain/core": "npm:^0.3.18"
|
||||
"@langchain/ollama": "npm:^0.1.2"
|
||||
"@mozilla/readability": "npm:^0.5.0"
|
||||
"@playwright/test": "npm:^1.48.2"
|
||||
@@ -12005,7 +11995,7 @@ __metadata:
|
||||
"@radix-ui/react-popover": "npm:^1.1.2"
|
||||
"@radix-ui/react-progress": "npm:^1.1.0"
|
||||
"@radix-ui/react-radio-group": "npm:^1.2.1"
|
||||
"@radix-ui/react-scroll-area": "npm:^1.2.0"
|
||||
"@radix-ui/react-scroll-area": "npm:^1.2.1"
|
||||
"@radix-ui/react-select": "npm:^2.1.2"
|
||||
"@radix-ui/react-separator": "npm:^1.1.0"
|
||||
"@radix-ui/react-slider": "npm:^1.2.1"
|
||||
@@ -12014,7 +12004,7 @@ __metadata:
|
||||
"@radix-ui/react-tabs": "npm:^1.1.1"
|
||||
"@radix-ui/react-toast": "npm:^1.2.2"
|
||||
"@radix-ui/react-toggle": "npm:^1.1.0"
|
||||
"@radix-ui/react-tooltip": "npm:^1.1.3"
|
||||
"@radix-ui/react-tooltip": "npm:^1.1.4"
|
||||
"@rails/actioncable": "npm:8.0.0"
|
||||
"@tailwindcss/typography": "npm:^0.5.15"
|
||||
"@types/ahoy.js": "npm:^0.4.2"
|
||||
@@ -12038,8 +12028,8 @@ __metadata:
|
||||
"@types/unzipper": "npm:^0.10.10"
|
||||
"@types/validator": "npm:^13.12.2"
|
||||
"@types/wavesurfer.js": "npm:^6.0.12"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^8.13.0"
|
||||
"@typescript-eslint/parser": "npm:^8.13.0"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^8.14.0"
|
||||
"@typescript-eslint/parser": "npm:^8.14.0"
|
||||
"@uidotdev/usehooks": "npm:^2.4.1"
|
||||
"@vidstack/react": "npm:^1.12.12"
|
||||
"@vitejs/plugin-react": "npm:^4.3.3"
|
||||
@@ -12061,7 +12051,7 @@ __metadata:
|
||||
dayjs: "npm:^1.11.13"
|
||||
decamelize: "npm:^6.0.0"
|
||||
decamelize-keys: "npm:^2.0.1"
|
||||
echogarden: "npm:^2.0.0"
|
||||
echogarden: "npm:2.0.3"
|
||||
electron: "npm:^33.2.0"
|
||||
electron-context-menu: "npm:^4.0.4"
|
||||
electron-devtools-installer: "npm:^3.2.0"
|
||||
@@ -12081,6 +12071,7 @@ __metadata:
|
||||
html-to-text: "npm:^9.0.5"
|
||||
https-proxy-agent: "npm:^7.0.5"
|
||||
i18next: "npm:^23.16.5"
|
||||
input-otp: "npm:^1.4.1"
|
||||
intl-tel-input: "npm:^24.7.0"
|
||||
js-md5: "npm:^0.8.3"
|
||||
langchain: "npm:^0.3.5"
|
||||
@@ -12088,14 +12079,15 @@ __metadata:
|
||||
lru-cache: "npm:^11.0.2"
|
||||
lucide-react: "npm:^0.456.0"
|
||||
mark.js: "npm:^8.11.1"
|
||||
media-captions: "npm:^0.0.18"
|
||||
microsoft-cognitiveservices-speech-sdk: "npm:^1.41.0"
|
||||
mime-types: "npm:^2.1.35"
|
||||
mustache: "npm:^4.2.0"
|
||||
next-themes: "npm:^0.4.3"
|
||||
octokit: "npm:^4.0.2"
|
||||
openai: "npm:^4.71.1"
|
||||
openai: "npm:^4.72.0"
|
||||
pitchfinder: "npm:^2.3.2"
|
||||
postcss: "npm:^8.4.48"
|
||||
postcss: "npm:^8.4.49"
|
||||
progress: "npm:^2.0.3"
|
||||
prop-types: "npm:^15.8.1"
|
||||
proxy-agent: "npm:^6.4.0"
|
||||
@@ -12133,12 +12125,12 @@ __metadata:
|
||||
umzug: "npm:^3.8.2"
|
||||
unzipper: "npm:^0.12.3"
|
||||
update-electron-app: "npm:^3.0.0"
|
||||
vite: "npm:^5.4.10"
|
||||
vite: "npm:^5.4.11"
|
||||
vite-plugin-static-copy: "npm:^2.1.0"
|
||||
wavesurfer.js: "npm:^7.8.8"
|
||||
zod: "npm:^3.23.8"
|
||||
zod-to-json-schema: "npm:^3.23.5"
|
||||
zx: "npm:^8.2.1"
|
||||
zx: "npm:^8.2.2"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -14309,13 +14301,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-escaper@npm:^3.0.3":
|
||||
version: 3.0.3
|
||||
resolution: "html-escaper@npm:3.0.3"
|
||||
checksum: 10c0/a042fa4139127ff7546513e90ea39cc9161a1938ce90122dbc4260d4b7252c9aa8452f4509c0c2889901b8ae9a8699179150f1f99d3f80bcf7317573c5f08f4e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-parse-stringify@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "html-parse-stringify@npm:3.0.1"
|
||||
@@ -14634,10 +14619,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"immutable@npm:^4.0.0":
|
||||
version: 4.3.7
|
||||
resolution: "immutable@npm:4.3.7"
|
||||
checksum: 10c0/9b099197081b22f6433003e34929da8ecddbbdc1474cdc8aa3b7669dee4adda349c06143de22def36016d1b6de5322b043eccd7a11db1dad2ca85dad4fff5435
|
||||
"immutable@npm:^5.0.2":
|
||||
version: 5.0.2
|
||||
resolution: "immutable@npm:5.0.2"
|
||||
checksum: 10c0/0d97ad95384e49563b6ed68f90e5ea83c149fd96ff417fae8274e1c524e3ef800eb1a2e8009e29d9b8ffdf63affd7692f87c8af72714181aad8dca88747fb5ac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -14765,6 +14750,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"input-otp@npm:^1.4.1":
|
||||
version: 1.4.1
|
||||
resolution: "input-otp@npm:1.4.1"
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
||||
checksum: 10c0/7630c5b2be54a0569d52579c8cc4a66dab5e17afff75adf889f878c2da35cebf391be5fd4f374884e2ca5514b563dfac46eb70f4047282ba8a3c91859ae8500b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"int64-buffer@npm:^0.1.9":
|
||||
version: 0.1.10
|
||||
resolution: "int64-buffer@npm:0.1.10"
|
||||
@@ -16683,6 +16678,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"media-captions@npm:^0.0.18":
|
||||
version: 0.0.18
|
||||
resolution: "media-captions@npm:0.0.18"
|
||||
checksum: 10c0/616ead02c3f755ab0a45f79cdc6a9204c6c157c450041e126e7d40a7e91fef5e562b343c497d276a9f99c44da23c12bcc53dbb9351c3c31448e19239dc22bc53
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"media-captions@npm:^1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "media-captions@npm:1.0.4"
|
||||
@@ -17518,13 +17520,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"moving-median@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "moving-median@npm:1.0.0"
|
||||
checksum: 10c0/e174657eb053f0423272524ee0dd7e8bd9a925945828b7b4a13a67b1c69ff617a0dd5cf28921cf1281c2bf7eda64f62bf79e73892bad75250f66f1d56489a208
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mri@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "mri@npm:1.2.0"
|
||||
@@ -18536,7 +18531,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openai@npm:^4.71.0, openai@npm:^4.71.1":
|
||||
"openai@npm:^4.71.0":
|
||||
version: 4.71.1
|
||||
resolution: "openai@npm:4.71.1"
|
||||
dependencies:
|
||||
@@ -18558,6 +18553,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openai@npm:^4.72.0":
|
||||
version: 4.72.0
|
||||
resolution: "openai@npm:4.72.0"
|
||||
dependencies:
|
||||
"@types/node": "npm:^18.11.18"
|
||||
"@types/node-fetch": "npm:^2.6.4"
|
||||
abort-controller: "npm:^3.0.0"
|
||||
agentkeepalive: "npm:^4.2.1"
|
||||
form-data-encoder: "npm:1.7.2"
|
||||
formdata-node: "npm:^4.3.2"
|
||||
node-fetch: "npm:^2.6.7"
|
||||
peerDependencies:
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
bin:
|
||||
openai: bin/cli
|
||||
checksum: 10c0/38d76a3359297b7a8f2b6780d03ccbc152985e39141e7cf7a7823d5b0b189990523c5fb6be26631eb191cb3c437e293023e7e9fd3c5c2ae37369175e838bc9e9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openapi-types@npm:^12.1.3":
|
||||
version: 12.1.3
|
||||
resolution: "openapi-types@npm:12.1.3"
|
||||
@@ -19704,14 +19721,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss@npm:^8.4.48":
|
||||
version: 8.4.48
|
||||
resolution: "postcss@npm:8.4.48"
|
||||
"postcss@npm:^8.4.49":
|
||||
version: 8.4.49
|
||||
resolution: "postcss@npm:8.4.49"
|
||||
dependencies:
|
||||
nanoid: "npm:^3.3.7"
|
||||
picocolors: "npm:^1.1.1"
|
||||
source-map-js: "npm:^1.2.1"
|
||||
checksum: 10c0/d586361fda12fc7ab5650ce9b5763fc61d6ea2cecac9da98fceea6a3f27e42ed34db830582411bc06743492d9bb414c52b0c81da65440682d244d692da2f928a
|
||||
checksum: 10c0/f1b3f17aaf36d136f59ec373459f18129908235e65dbdc3aee5eef8eba0756106f52de5ec4682e29a2eab53eb25170e7e871b3e4b52a8f1de3d344a514306be3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -20978,20 +20995,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sass@npm:^1.80.6":
|
||||
version: 1.80.6
|
||||
resolution: "sass@npm:1.80.6"
|
||||
"sass@npm:^1.80.7":
|
||||
version: 1.80.7
|
||||
resolution: "sass@npm:1.80.7"
|
||||
dependencies:
|
||||
"@parcel/watcher": "npm:^2.4.1"
|
||||
chokidar: "npm:^4.0.0"
|
||||
immutable: "npm:^4.0.0"
|
||||
immutable: "npm:^5.0.2"
|
||||
source-map-js: "npm:>=0.6.2 <2.0.0"
|
||||
dependenciesMeta:
|
||||
"@parcel/watcher":
|
||||
optional: true
|
||||
bin:
|
||||
sass: sass.js
|
||||
checksum: 10c0/04ce40d4dcf06cf2a94a66c1cc4fd4a9eb4033fd039291acd0be9d1d4123860da568c5cbef9de8493ffbedd8acae1cd0b8346f5da21c6f7cf0ffd3477730beca
|
||||
checksum: 10c0/e0e0df8dc9dd7694826f915196a96cda45fe0fc849be9fc08b43c12aa1250eb512130979ed239e1106476973ace1f52abbcc1d5900a075d3813c282a626dcbf7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -22371,21 +22388,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tldts-core@npm:^6.1.59":
|
||||
version: 6.1.59
|
||||
resolution: "tldts-core@npm:6.1.59"
|
||||
checksum: 10c0/fa182200fe69e35ae8fbdf96bf327831f25a6c10d85d038b80cef11a2ac9a6fa68c145f92f1d06da768fa0805619c93cee471d7c70acb7f8329d3fb735022fb1
|
||||
"tldts-core@npm:^6.1.61":
|
||||
version: 6.1.61
|
||||
resolution: "tldts-core@npm:6.1.61"
|
||||
checksum: 10c0/4596569079488af159ebf5db5d15dee4773314b01c8e3ce7c05dbe149b7670d715ac41fb9d34b7c6333e382fbeb7c9c0314be995176c0a357977936fd1225903
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tldts@npm:^6.1.32":
|
||||
version: 6.1.59
|
||||
resolution: "tldts@npm:6.1.59"
|
||||
version: 6.1.61
|
||||
resolution: "tldts@npm:6.1.61"
|
||||
dependencies:
|
||||
tldts-core: "npm:^6.1.59"
|
||||
tldts-core: "npm:^6.1.61"
|
||||
bin:
|
||||
tldts: bin/cli.js
|
||||
checksum: 10c0/2e8eaf6ff5dc25ac59df27908a2eab609b86c26e1b0a63639ac2dd58e38230e9113be4e33ac7607c04dc1e6fc2c33b6b6e025919c8780ad53374e9d2ae867d1d
|
||||
checksum: 10c0/6c9b43b5842f5fc79201b86a904c96ce26d96ad746e8227db15cb27cb8271b434e99ac71f7ee782cc1c72fb530e3e8a7b8c463685b7cb418924bf95a32fec000
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -23663,6 +23680,49 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite@npm:^5.4.11":
|
||||
version: 5.4.11
|
||||
resolution: "vite@npm:5.4.11"
|
||||
dependencies:
|
||||
esbuild: "npm:^0.21.3"
|
||||
fsevents: "npm:~2.3.3"
|
||||
postcss: "npm:^8.4.43"
|
||||
rollup: "npm:^4.20.0"
|
||||
peerDependencies:
|
||||
"@types/node": ^18.0.0 || >=20.0.0
|
||||
less: "*"
|
||||
lightningcss: ^1.21.0
|
||||
sass: "*"
|
||||
sass-embedded: "*"
|
||||
stylus: "*"
|
||||
sugarss: "*"
|
||||
terser: ^5.4.0
|
||||
dependenciesMeta:
|
||||
fsevents:
|
||||
optional: true
|
||||
peerDependenciesMeta:
|
||||
"@types/node":
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
bin:
|
||||
vite: bin/vite.js
|
||||
checksum: 10c0/d536bb7af57dd0eca2a808f95f5ff1d7b7ffb8d86e17c6893087680a0448bd0d15e07475270c8a6de65cb5115592d037130a1dd979dc76bcef8c1dda202a1874
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vitepress-plugin-mermaid@npm:^2.0.17":
|
||||
version: 2.0.17
|
||||
resolution: "vitepress-plugin-mermaid@npm:2.0.17"
|
||||
@@ -24437,9 +24497,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zx@npm:^8.2.1":
|
||||
version: 8.2.1
|
||||
resolution: "zx@npm:8.2.1"
|
||||
"zx@npm:^8.2.2":
|
||||
version: 8.2.2
|
||||
resolution: "zx@npm:8.2.2"
|
||||
dependencies:
|
||||
"@types/fs-extra": "npm:>=11"
|
||||
"@types/node": "npm:>=20"
|
||||
@@ -24450,6 +24510,6 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
zx: build/cli.js
|
||||
checksum: 10c0/39aac596a031eb149d91c54359ab74969e5135ce3de401dabcae06b16516fdf28aa97c01b5c2509e46a5bcf9d3d941f349e5ac2df861f67aa6755df84e629bc5
|
||||
checksum: 10c0/68c2fd54cd10e2c3b32ae7db62cc0004808b7ece6aad2c87838e354174bd9868918cc1a6e067d6a7647cfb1180253e6a362f8b57aabb4b2895c8f6c0e92a62cd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
Reference in New Issue
Block a user